1
0

GameTicker.Replays.cs 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153
  1. using Content.Shared.CCVar;
  2. using Robust.Shared;
  3. using Robust.Shared.ContentPack;
  4. using Robust.Shared.Replays;
  5. using Robust.Shared.Serialization.Manager;
  6. using Robust.Shared.Serialization.Markdown;
  7. using Robust.Shared.Serialization.Markdown.Mapping;
  8. using Robust.Shared.Serialization.Markdown.Value;
  9. using Robust.Shared.Utility;
  10. namespace Content.Server.GameTicking;
  11. public sealed partial class GameTicker
  12. {
  13. [Dependency] private readonly IReplayRecordingManager _replays = default!;
  14. [Dependency] private readonly IResourceManager _resourceManager = default!;
  15. [Dependency] private readonly ISerializationManager _serialman = default!;
  16. private ISawmill _sawmillReplays = default!;
  17. private void InitializeReplays()
  18. {
  19. _replays.RecordingFinished += ReplaysOnRecordingFinished;
  20. _replays.RecordingStopped += ReplaysOnRecordingStopped;
  21. }
  22. /// <summary>
  23. /// A round has started: start recording replays if auto record is enabled.
  24. /// </summary>
  25. private void ReplayStartRound()
  26. {
  27. try
  28. {
  29. if (!_cfg.GetCVar(CCVars.ReplayAutoRecord))
  30. return;
  31. if (_replays.IsRecording)
  32. {
  33. _sawmillReplays.Warning("Already an active replay recording before the start of the round, not starting automatic recording.");
  34. return;
  35. }
  36. _sawmillReplays.Debug($"Starting replay recording for round {RoundId}");
  37. var finalPath = GetAutoReplayPath();
  38. var recordPath = finalPath;
  39. var tempDir = _cfg.GetCVar(CCVars.ReplayAutoRecordTempDir);
  40. ResPath? moveToPath = null;
  41. // Set the round end player and text back to null to prevent it from writing the previous round's data.
  42. _replayRoundPlayerInfo = null;
  43. _replayRoundText = null;
  44. if (!string.IsNullOrEmpty(tempDir))
  45. {
  46. var baseReplayPath = new ResPath(_cfg.GetCVar(CVars.ReplayDirectory)).ToRootedPath();
  47. moveToPath = baseReplayPath / finalPath;
  48. var fileName = finalPath.Filename;
  49. recordPath = new ResPath(tempDir) / fileName;
  50. _sawmillReplays.Debug($"Replay will record in temporary position: {recordPath}");
  51. }
  52. var recordState = new ReplayRecordState(moveToPath);
  53. if (!_replays.TryStartRecording(_resourceManager.UserData, recordPath.ToString(), state: recordState))
  54. {
  55. _sawmillReplays.Error("Can't start automatic replay recording!");
  56. }
  57. }
  58. catch (Exception e)
  59. {
  60. Log.Error($"Error while starting an automatic replay recording:\n{e}");
  61. }
  62. }
  63. /// <summary>
  64. /// A round has ended: stop recording replays and make sure they're moved to the correct spot.
  65. /// </summary>
  66. private void ReplayEndRound()
  67. {
  68. try
  69. {
  70. if (_replays.ActiveRecordingState is ReplayRecordState)
  71. {
  72. _replays.StopRecording();
  73. }
  74. }
  75. catch (Exception e)
  76. {
  77. Log.Error($"Error while stopping replay recording:\n{e}");
  78. }
  79. }
  80. private void ReplaysOnRecordingFinished(ReplayRecordingFinished data)
  81. {
  82. if (data.State is not ReplayRecordState state)
  83. return;
  84. if (state.MoveToPath == null)
  85. return;
  86. _sawmillReplays.Info($"Moving replay into final position: {state.MoveToPath}");
  87. _taskManager.BlockWaitOnTask(_replays.WaitWriteTasks());
  88. DebugTools.Assert(!_replays.IsWriting());
  89. try
  90. {
  91. if (!data.Directory.Exists(state.MoveToPath.Value.Directory))
  92. data.Directory.CreateDir(state.MoveToPath.Value.Directory);
  93. }
  94. catch (UnauthorizedAccessException e)
  95. {
  96. _sawmillReplays.Error($"Error creating replay directory {state.MoveToPath.Value.Directory}: {e}");
  97. }
  98. data.Directory.Rename(data.Path, state.MoveToPath.Value);
  99. }
  100. private void ReplaysOnRecordingStopped(MappingDataNode metadata)
  101. {
  102. // Write round info like map and round end summery into the replay_final.yml file. Useful for external parsers.
  103. metadata["map"] = new ValueDataNode(_gameMapManager.GetSelectedMap()?.MapName);
  104. metadata["gamemode"] = new ValueDataNode(CurrentPreset != null ? Loc.GetString(CurrentPreset.ModeTitle) : string.Empty);
  105. metadata["roundEndPlayers"] = _serialman.WriteValue(_replayRoundPlayerInfo);
  106. metadata["roundEndText"] = new ValueDataNode(_replayRoundText);
  107. metadata["server_id"] = new ValueDataNode(_cfg.GetCVar(CCVars.ServerId));
  108. metadata["server_name"] = new ValueDataNode(_cfg.GetCVar(CCVars.AdminLogsServerName));
  109. metadata["roundId"] = new ValueDataNode(RoundId.ToString());
  110. }
  111. private ResPath GetAutoReplayPath()
  112. {
  113. var cfgValue = _cfg.GetCVar(CCVars.ReplayAutoRecordName);
  114. var time = DateTime.UtcNow;
  115. var interpolated = cfgValue
  116. .Replace("{year}", time.Year.ToString("D4"))
  117. .Replace("{month}", time.Month.ToString("D2"))
  118. .Replace("{day}", time.Day.ToString("D2"))
  119. .Replace("{hour}", time.Hour.ToString("D2"))
  120. .Replace("{minute}", time.Minute.ToString("D2"))
  121. .Replace("{round}", RoundId.ToString());
  122. return new ResPath(interpolated);
  123. }
  124. private sealed record ReplayRecordState(ResPath? MoveToPath);
  125. }