Files
RobustToolbox/Robust.Client/Replays/Playback/ReplayPlaybackManager.cs
T
Pieter-Jan Briers 83c2a1be11 [Dependency] source generator part 2 (#6550)
* [Dependency] source generator

No more reflection, no more codegen at runtime

Also various changes to Roslyn helpers to make this easier to write.

Requires all types with dependencies to be partial and not have readonly dependency fields. An analyzer enforces this at warning level, the previous injection strategies have remained in the code *for now* as a fallback.

No fallback is available for [field: Dependency] properties, due to a Roslyn bug.

Code Fixes exist. We love Roslyn

* Apply dependencies generator changes to all code

* Release notes

* Preprocessor got hands

* Handle nullable dependencies

These are bad but gotta deal with it.

* Apply suggestions from code review

Co-authored-by: Moony <moony@hellomouse.net>

* Fine, let's not use collection expressions

---------

Co-authored-by: Moony <moony@hellomouse.net>
2026-05-08 12:38:33 +02:00

173 lines
5.6 KiB
C#

using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using Robust.Client.Audio;
using Robust.Client.Audio.Midi;
using Robust.Client.Configuration;
using Robust.Client.GameObjects;
using Robust.Client.GameStates;
using Robust.Client.Graphics;
using Robust.Client.Player;
using Robust.Client.Timing;
using Robust.Client.Upload;
using Robust.Shared;
using Robust.Shared.Audio;
using Robust.Shared.Configuration;
using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Network;
using Robust.Shared.Prototypes;
using Robust.Shared.Replays;
using Robust.Shared.Serialization.Markdown.Mapping;
namespace Robust.Client.Replays.Playback;
internal sealed partial class ReplayPlaybackManager : IReplayPlaybackManager
{
[Dependency] private ILogManager _logMan = default!;
[Dependency] private IBaseClient _client = default!;
[Dependency] private IMidiManager _midi = default!;
[Dependency] private IPlayerManager _player = default!;
[Dependency] private IAudioInternal _clydeAudio = default!;
[Dependency] private IClientGameTiming _timing = default!;
[Dependency] private IClientNetManager _netMan = default!;
[Dependency] private IComponentFactory _factory = default!;
[Dependency] private IPrototypeManager _protoMan = default!;
[Dependency] private IGameController _controller = default!;
[Dependency] private IClientEntityManager _cEntManager = default!;
[Dependency] private ClientEntityManager _entMan = default!;
[Dependency] private IConfigurationManager _confMan = default!;
[Dependency] private NetworkResourceManager _netResMan = default!;
[Dependency] private IClientGameStateManager _gameState = default!;
[Dependency] private IClientNetConfigurationManager _netConf = default!;
public event Action<MappingDataNode, List<object>>? ReplayPlaybackStarted;
public event Action? ReplayPlaybackStopped;
public event Action? ReplayPaused;
public event Action? ReplayUnpaused;
public event Action<(GameState Current, GameState? Next)>? BeforeApplyState;
public ReplayData? Replay { get; private set; }
public NetUserId? Recorder => Replay?.Recorder;
private int _checkpointMinInterval;
private int _replayMaxScrubTime;
private int _visualEventThreshold;
public uint? AutoPauseCountdown { get; set; }
public int? ScrubbingTarget { get; set; }
private bool _playing;
private ushort _metaId;
private bool _initialized;
private ISawmill _sawmill = default!;
private HashSet<Type> _warned = new();
public bool Playing
{
get => Replay != null && _playing && ScrubbingTarget == null;
set
{
if (Replay == null || _playing == value)
return;
_playing = value;
if (_playing)
{
ReplayUnpaused?.Invoke();
}
else
{
StopAudio();
AutoPauseCountdown = null;
ReplayPaused?.Invoke();
}
}
}
public void Initialize()
{
if (_initialized)
return;
_initialized = true;
_sawmill = _logMan.GetSawmill("replay");
_metaId = _factory.GetRegistration(typeof(MetaDataComponent)).NetID!.Value;
_confMan.OnValueChanged(CVars.CheckpointMinInterval, (value) => _checkpointMinInterval = value, true);
_confMan.OnValueChanged(CVars.ReplayMaxScrubTime, (value) => _replayMaxScrubTime = value, true);
_confMan.OnValueChanged(CVars.ReplaySkipThreshold, (value) => _visualEventThreshold = value, true);
_client.RunLevelChanged += OnRunLevelChanged;
}
private void OnRunLevelChanged(object? sender, RunLevelChangedEventArgs e)
{
if (Replay == null || e.NewLevel == ClientRunLevel.SinglePlayerGame)
return;
_sawmill.Error($"Run level changed without stopping the current replay. Trace: {Environment.StackTrace}");
StopReplay();
}
public void StartReplay(ReplayData replay)
{
if (Replay != null)
throw new Exception("Already playing a replay");
if (_client.RunLevel != ClientRunLevel.SinglePlayerGame)
throw new Exception($"Invalid runlevel: {_client.RunLevel}.");
Replay = replay;
_controller.TickUpdateOverride += TickUpdateOverride;
if (Replay.CurrentIndex < 0)
ResetToNearestCheckpoint(0, true);
ReplayPlaybackStarted?.Invoke(Replay.YamlData, Replay.InitialMessages?.Messages ?? new ());
}
public void StopReplay()
{
if (Replay == null)
return;
_playing = false;
Replay.CurrentIndex = -1;
Replay = null;
_controller.TickUpdateOverride -= TickUpdateOverride;
_entMan.FlushEntities();
// Unload any uploaded prototypes & resources.
_netResMan.ClearResources();
_protoMan.Reset();
ReplayPlaybackStopped?.Invoke();
}
public void StopAudio()
{
_clydeAudio.StopAllAudio();
foreach (var renderer in _midi.Renderers)
{
renderer.ClearAllEvents();
renderer.StopAllNotes();
}
}
public bool TryGetRecorderEntity([NotNullWhen(true)] out EntityUid? uid)
{
if (Recorder != null
&& _player.SessionsDict.TryGetValue(Recorder.Value, out var session)
&& session.AttachedEntity is { } recorderEnt
&& _entMan.EntityExists(recorderEnt))
{
uid = recorderEnt;
return true;
}
uid = null;
return false;
}
}