Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 1 commit into from
Aug 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions Robust.Shared/Replays/IReplayRecordingManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -131,6 +141,27 @@ bool TryStartRecording(
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>
Expand All @@ -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>
Expand Down
29 changes: 29 additions & 0 deletions Robust.Shared/Replays/SharedReplayRecordingManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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!;
Expand Down Expand Up @@ -312,6 +313,7 @@ protected virtual void Reset()

// File stream & compression context is always disposed from the worker task.
_recState.WriteCommandChannel.Complete();
_recState.Done = true;

_recState = null;
}
Expand Down Expand Up @@ -373,6 +375,11 @@ private void WriteFinalMetadata(RecordingState recState)
{
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());
Expand All @@ -384,6 +391,7 @@ private void WriteFinalMetadata(RecordingState recState)
// this just overwrites the previous yml with additional data.
var document = new YamlDocument(yamlMetadata.ToYaml());
WriteYaml(recState, ReplayZipFolder / FileMetaFinal, document);

UpdateWriteTasks();
Reset();

Expand Down Expand Up @@ -492,6 +500,8 @@ private sealed class RecordingState
public long CompressedSize;
public long UncompressedSize;

public bool Done;

public RecordingState(
ZipArchive zip,
MemoryStream buffer,
Expand All @@ -518,4 +528,23 @@ public RecordingState(
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));
}
}
}
Loading