Add support for client-side replays (#17168)
This commit is contained in:
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -84,6 +84,7 @@ public sealed partial class ReplaySpectatorSystem
|
||||
|
||||
_stateMan.RequestStateChange<ReplayGhostState>();
|
||||
|
||||
_spectatorData = GetSpectatorData();
|
||||
return xform;
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user