Add support for client-side replays (#17168)

This commit is contained in:
Leon Friedrich
2023-06-19 05:23:31 +12:00
committed by GitHub
parent cc81a7511b
commit b03d9a90ab
22 changed files with 183 additions and 91 deletions

View File

@@ -11,6 +11,7 @@ using Robust.Shared.Map;
using Robust.Shared.Player;
using Robust.Shared.Players;
using Robust.Shared.Prototypes;
using Robust.Shared.Replays;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
@@ -26,6 +27,7 @@ namespace Content.Client.Popups
[Dependency] private readonly IResourceCache _resource = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly IUserInterfaceManager _uiManager = default!;
[Dependency] private readonly IReplayRecordingManager _replayRecording = default!;
public IReadOnlyList<WorldPopupLabel> WorldLabels => _aliveWorldLabels;
public IReadOnlyList<CursorPopupLabel> CursorLabels => _aliveCursorLabels;
@@ -52,8 +54,16 @@ namespace Content.Client.Popups
.RemoveOverlay<PopupOverlay>();
}
private void PopupMessage(string message, PopupType type, EntityCoordinates coordinates, EntityUid? entity = null)
private void PopupMessage(string message, PopupType type, EntityCoordinates coordinates, EntityUid? entity, bool recordReplay)
{
if (recordReplay && _replayRecording.IsRecording)
{
if (entity != null)
_replayRecording.RecordClientMessage(new PopupEntityEvent(message, type, entity.Value));
else
_replayRecording.RecordClientMessage(new PopupCoordinatesEvent(message, type, coordinates));
}
var label = new WorldPopupLabel(coordinates)
{
Text = message,
@@ -66,23 +76,26 @@ namespace Content.Client.Popups
#region Abstract Method Implementations
public override void PopupCoordinates(string message, EntityCoordinates coordinates, PopupType type = PopupType.Small)
{
PopupMessage(message, type, coordinates, null);
PopupMessage(message, type, coordinates, null, true);
}
public override void PopupCoordinates(string message, EntityCoordinates coordinates, ICommonSession recipient, PopupType type = PopupType.Small)
{
if (_playerManager.LocalPlayer?.Session == recipient)
PopupMessage(message, type, coordinates, null);
PopupMessage(message, type, coordinates, null, true);
}
public override void PopupCoordinates(string message, EntityCoordinates coordinates, EntityUid recipient, PopupType type = PopupType.Small)
{
if (_playerManager.LocalPlayer?.ControlledEntity == recipient)
PopupMessage(message, type, coordinates, null);
PopupMessage(message, type, coordinates, null, true);
}
public override void PopupCursor(string message, PopupType type = PopupType.Small)
private void PopupCursorInternal(string message, PopupType type, bool recordReplay)
{
if (recordReplay && _replayRecording.IsRecording)
_replayRecording.RecordClientMessage(new PopupCursorEvent(message, type));
var label = new CursorPopupLabel(_inputManager.MouseScreenPosition)
{
Text = message,
@@ -92,6 +105,9 @@ namespace Content.Client.Popups
_aliveCursorLabels.Add(label);
}
public override void PopupCursor(string message, PopupType type = PopupType.Small)
=> PopupCursorInternal(message, type, true);
public override void PopupCursor(string message, ICommonSession recipient, PopupType type = PopupType.Small)
{
if (_playerManager.LocalPlayer?.Session == recipient)
@@ -137,12 +153,8 @@ namespace Content.Client.Popups
public override void PopupEntity(string message, EntityUid uid, PopupType type = PopupType.Small)
{
if (!EntityManager.EntityExists(uid))
return;
var transform = EntityManager.GetComponent<TransformComponent>(uid);
PopupMessage(message, type, transform.Coordinates, uid);
if (TryComp(uid, out TransformComponent? transform))
PopupMessage(message, type, transform.Coordinates, uid, true);
}
#endregion
@@ -151,17 +163,18 @@ namespace Content.Client.Popups
private void OnPopupCursorEvent(PopupCursorEvent ev)
{
PopupCursor(ev.Message, ev.Type);
PopupCursorInternal(ev.Message, ev.Type, false);
}
private void OnPopupCoordinatesEvent(PopupCoordinatesEvent ev)
{
PopupCoordinates(ev.Message, ev.Coordinates, ev.Type);
PopupMessage(ev.Message, ev.Type, ev.Coordinates, null, false);
}
private void OnPopupEntityEvent(PopupEntityEvent ev)
{
PopupEntity(ev.Message, ev.Uid, ev.Type);
if (TryComp(ev.Uid, out TransformComponent? transform))
PopupMessage(ev.Message, ev.Type, transform.Coordinates, ev.Uid, false);
}
private void OnRoundRestart(RoundRestartCleanupEvent ev)

View File

@@ -22,6 +22,7 @@ using Robust.Client.State;
using Robust.Client.Timing;
using Robust.Client.UserInterface;
using Robust.Shared.ContentPack;
using Robust.Shared.Serialization.Markdown.Mapping;
using Robust.Shared.Utility;
namespace Content.Client.Replay;
@@ -99,6 +100,8 @@ public sealed class ContentReplayPlaybackManager
{
switch (message)
{
case BoundUserInterfaceMessage:
break; // TODO REPLAYS refactor BUIs
case ChatMessage chat:
// Just pass on the chat message to the UI controller, but skip speech-bubbles if we are fast-forwarding.
_uiMan.GetUIController<ChatUIController>().ProcessChatMessage(chat, speechBubble: !skipEffects);
@@ -129,8 +132,7 @@ public sealed class ContentReplayPlaybackManager
return false;
}
private void OnReplayPlaybackStarted()
private void OnReplayPlaybackStarted(MappingDataNode metadata, List<object> objects)
{
_conGrp.Implementation = new ReplayConGroup();
}

View File

@@ -3,6 +3,7 @@ using Content.Shared.Movement.Components;
using Robust.Client.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Network;
namespace Content.Client.Replay.Spectator;
@@ -13,61 +14,99 @@ public sealed partial class ReplaySpectatorSystem
/// <summary>
/// Simple struct containing position & rotation data for maintaining a persistent view when jumping around in time.
/// </summary>
public struct SpectatorPosition
public struct SpectatorData
{
// TODO REPLAYS handle ghost-following.
/// <summary>
/// The current entity being spectated.
/// </summary>
public EntityUid Entity;
/// <summary>
/// The player that was originally controlling <see cref="Entity"/>
/// </summary>
public NetUserId? Controller;
public (EntityCoordinates Coords, Angle Rot)? Local;
public (EntityCoordinates Coords, Angle Rot)? World;
public (EntityUid? Ent, Angle Rot)? Eye;
}
public SpectatorPosition GetSpectatorPosition()
public SpectatorData GetSpectatorData()
{
var obs = new SpectatorPosition();
if (_player.LocalPlayer?.ControlledEntity is { } player && TryComp(player, out TransformComponent? xform) && xform.MapUid != null)
var data = new SpectatorData();
if (_player.LocalPlayer?.ControlledEntity is not { } player)
return data;
foreach (var session in _player.Sessions)
{
obs.Local = (xform.Coordinates, xform.LocalRotation);
obs.World = (new(xform.MapUid.Value, xform.WorldPosition), xform.WorldRotation);
if (session.UserId == _player.LocalPlayer?.UserId)
continue;
if (TryComp(player, out InputMoverComponent? mover))
obs.Eye = (mover.RelativeEntity, mover.TargetRelativeRotation);
obs.Entity = player;
if (session.AttachedEntity == player)
{
data.Controller = session.UserId;
break;
}
}
return obs;
if (!TryComp(player, out TransformComponent? xform) || xform.MapUid == null)
return data;
data.Local = (xform.Coordinates, xform.LocalRotation);
data.World = (new(xform.MapUid.Value, xform.WorldPosition), xform.WorldRotation);
if (TryComp(player, out InputMoverComponent? mover))
data.Eye = (mover.RelativeEntity, mover.TargetRelativeRotation);
data.Entity = player;
return data;
}
private void OnBeforeSetTick()
{
_oldPosition = GetSpectatorPosition();
_spectatorData = GetSpectatorData();
}
private void OnAfterSetTick()
{
if (_oldPosition != null)
SetSpectatorPosition(_oldPosition.Value);
_oldPosition = null;
if (_spectatorData != null)
SetSpectatorPosition(_spectatorData.Value);
_spectatorData = null;
}
public void SetSpectatorPosition(SpectatorPosition spectatorPosition)
public void SetSpectatorPosition(SpectatorData data)
{
if (Exists(spectatorPosition.Entity) && Transform(spectatorPosition.Entity).MapID != MapId.Nullspace)
if (_player.LocalPlayer == null)
return;
if (data.Controller != null
&& _player.SessionsDict.TryGetValue(data.Controller.Value, out var session)
&& Exists(session.AttachedEntity)
&& Transform(session.AttachedEntity.Value).MapID != MapId.Nullspace)
{
_player.LocalPlayer!.AttachEntity(spectatorPosition.Entity, EntityManager, _client);
_player.LocalPlayer.AttachEntity(session.AttachedEntity.Value, EntityManager, _client);
return;
}
if (spectatorPosition.Local != null && spectatorPosition.Local.Value.Coords.IsValid(EntityManager))
if (Exists(data.Entity) && Transform(data.Entity).MapID != MapId.Nullspace)
{
var newXform = SpawnSpectatorGhost(spectatorPosition.Local.Value.Coords, false);
newXform.LocalRotation = spectatorPosition.Local.Value.Rot;
_player.LocalPlayer.AttachEntity(data.Entity, EntityManager, _client);
return;
}
else if (spectatorPosition.World != null && spectatorPosition.World.Value.Coords.IsValid(EntityManager))
if (data.Local != null && data.Local.Value.Coords.IsValid(EntityManager))
{
var newXform = SpawnSpectatorGhost(spectatorPosition.World.Value.Coords, true);
newXform.LocalRotation = spectatorPosition.World.Value.Rot;
var newXform = SpawnSpectatorGhost(data.Local.Value.Coords, false);
newXform.LocalRotation = data.Local.Value.Rot;
}
else if (data.World != null && data.World.Value.Coords.IsValid(EntityManager))
{
var newXform = SpawnSpectatorGhost(data.World.Value.Coords, true);
newXform.LocalRotation = data.World.Value.Rot;
}
else if (TryFindFallbackSpawn(out var coords))
{
@@ -80,15 +119,21 @@ public sealed partial class ReplaySpectatorSystem
return;
}
if (spectatorPosition.Eye != null && TryComp(_player.LocalPlayer?.ControlledEntity, out InputMoverComponent? newMover))
if (data.Eye != null && TryComp(_player.LocalPlayer.ControlledEntity, out InputMoverComponent? newMover))
{
newMover.RelativeEntity = spectatorPosition.Eye.Value.Ent;
newMover.TargetRelativeRotation = newMover.RelativeRotation = spectatorPosition.Eye.Value.Rot;
newMover.RelativeEntity = data.Eye.Value.Ent;
newMover.TargetRelativeRotation = newMover.RelativeRotation = data.Eye.Value.Rot;
}
}
private bool TryFindFallbackSpawn(out EntityCoordinates coords)
{
if (_replayPlayback.TryGetRecorderEntity(out var recorder))
{
coords = new EntityCoordinates(recorder.Value, default);
return true;
}
var uid = EntityQuery<MapGridComponent>()
.OrderByDescending(x => x.LocalAABB.Size.LengthSquared)
.FirstOrDefault()?.Owner;

View File

@@ -84,6 +84,7 @@ public sealed partial class ReplaySpectatorSystem
_stateMan.RequestStateChange<ReplayGhostState>();
_spectatorData = GetSpectatorData();
return xform;
}

View File

@@ -6,6 +6,8 @@ using Robust.Client.Player;
using Robust.Client.Replays.Playback;
using Robust.Client.State;
using Robust.Shared.Console;
using Robust.Shared.Network;
using Robust.Shared.Serialization.Markdown.Mapping;
namespace Content.Client.Replay.Spectator;
@@ -29,7 +31,7 @@ public sealed partial class ReplaySpectatorSystem : EntitySystem
[Dependency] private readonly SharedContentEyeSystem _eye = default!;
[Dependency] private readonly IReplayPlaybackManager _replayPlayback = default!;
private SpectatorPosition? _oldPosition;
private SpectatorData? _spectatorData;
public const string SpectateCmd = "replay_spectate";
public override void Initialize()
@@ -58,15 +60,19 @@ public sealed partial class ReplaySpectatorSystem : EntitySystem
_replayPlayback.ReplayPlaybackStopped -= OnPlaybackStopped;
}
private void OnPlaybackStarted()
private void OnPlaybackStarted(MappingDataNode yamlMappingNode, List<object> objects)
{
InitializeMovement();
SetSpectatorPosition(default);
_conHost.RegisterCommand(SpectateCmd,
Loc.GetString("cmd-replay-spectate-desc"),
Loc.GetString("cmd-replay-spectate-help"),
SpectateCommand,
SpectateCompletions);
if (_replayPlayback.TryGetRecorderEntity(out var recorder))
SpectateEntity(recorder.Value);
else
SetSpectatorPosition(default);
}
private void OnPlaybackStopped()

View File

@@ -66,9 +66,10 @@ public sealed class SubFloorHideSystem : SharedSubFloorHideSystem
private void UpdateAll()
{
foreach (var (_, appearance) in EntityManager.EntityQuery<SubFloorHideComponent, AppearanceComponent>(true))
var query = AllEntityQuery<SubFloorHideComponent, AppearanceComponent>();
while (query.MoveNext(out var uid, out _, out var appearance))
{
_appearance.MarkDirty(appearance, true);
_appearance.QueueUpdate(uid, appearance);
}
}
}

View File

@@ -29,6 +29,7 @@ using Robust.Shared.Configuration;
using Robust.Shared.Input.Binding;
using Robust.Shared.Map;
using Robust.Shared.Network;
using Robust.Shared.Replays;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
@@ -46,6 +47,8 @@ public sealed class ChatUIController : UIController
[Dependency] private readonly IPlayerManager _player = default!;
[Dependency] private readonly IStateManager _state = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly IReplayRecordingManager _replayRecording = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
[UISystemDependency] private readonly ExamineSystem? _examine = default;
[UISystemDependency] private readonly GhostSystem? _ghost = default;
@@ -758,7 +761,17 @@ public sealed class ChatUIController : UIController
_manager.SendMessage(text, prefixChannel == 0 ? channel : prefixChannel);
}
private void OnChatMessage(MsgChatMessage message) => ProcessChatMessage(message.Message);
private void OnChatMessage(MsgChatMessage message)
{
var msg = message.Message;
ProcessChatMessage(msg);
if ((msg.Channel & ChatChannel.AdminRelated) == 0 ||
_cfg.GetCVar(CCVars.ReplayRecordAdminChat))
{
_replayRecording.RecordClientMessage(msg);
}
}
public void ProcessChatMessage(ChatMessage msg, bool speechBubble = true)
{