Fix replay spectating bugs (#21573)
This commit is contained in:
@@ -40,6 +40,7 @@ public sealed class ContentReplayPlaybackManager
|
|||||||
[Dependency] private readonly IClientConGroupController _conGrp = default!;
|
[Dependency] private readonly IClientConGroupController _conGrp = default!;
|
||||||
[Dependency] private readonly IClientAdminManager _adminMan = default!;
|
[Dependency] private readonly IClientAdminManager _adminMan = default!;
|
||||||
[Dependency] private readonly IPlayerManager _player = default!;
|
[Dependency] private readonly IPlayerManager _player = default!;
|
||||||
|
[Dependency] private readonly IBaseClient _client = default!;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// UI state to return to when stopping a replay or loading fails.
|
/// UI state to return to when stopping a replay or loading fails.
|
||||||
@@ -87,6 +88,9 @@ public sealed class ContentReplayPlaybackManager
|
|||||||
_stateMan.RequestStateChange<LauncherConnecting>().SetDisconnected();
|
_stateMan.RequestStateChange<LauncherConnecting>().SetDisconnected();
|
||||||
else
|
else
|
||||||
_stateMan.RequestStateChange<MainScreen>();
|
_stateMan.RequestStateChange<MainScreen>();
|
||||||
|
|
||||||
|
if (_client.RunLevel == ClientRunLevel.SinglePlayerGame)
|
||||||
|
_client.StopSinglePlayer();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnCheckpointReset()
|
private void OnCheckpointReset()
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ public sealed partial class ReplaySpectatorSystem
|
|||||||
if (_replayPlayback.Replay == null)
|
if (_replayPlayback.Replay == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (_player.LocalPlayer?.ControlledEntity is not { } player)
|
if (_player.LocalEntity is not { } player)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (Direction == DirectionFlag.None)
|
if (Direction == DirectionFlag.None)
|
||||||
@@ -99,7 +99,7 @@ public sealed partial class ReplaySpectatorSystem
|
|||||||
var worldVec = parentRotation.RotateVec(localVec);
|
var worldVec = parentRotation.RotateVec(localVec);
|
||||||
var speed = CompOrNull<MovementSpeedModifierComponent>(player)?.BaseSprintSpeed ?? DefaultSpeed;
|
var speed = CompOrNull<MovementSpeedModifierComponent>(player)?.BaseSprintSpeed ?? DefaultSpeed;
|
||||||
var delta = worldVec * frameTime * speed;
|
var delta = worldVec * frameTime * speed;
|
||||||
_transform.SetWorldPositionRotation(xform, pos + delta, delta.ToWorldAngle());
|
_transform.SetWorldPositionRotation(player, pos + delta, delta.ToWorldAngle(), xform);
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed class MoverHandler : InputCmdHandler
|
private sealed class MoverHandler : InputCmdHandler
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using Content.Shared.Movement.Components;
|
using Content.Shared.Movement.Components;
|
||||||
|
using Robust.Shared.GameStates;
|
||||||
using Robust.Shared.Map;
|
using Robust.Shared.Map;
|
||||||
using Robust.Shared.Map.Components;
|
using Robust.Shared.Map.Components;
|
||||||
using Robust.Shared.Network;
|
using Robust.Shared.Network;
|
||||||
@@ -25,7 +26,7 @@ public sealed partial class ReplaySpectatorSystem
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// The player that was originally controlling <see cref="Entity"/>
|
/// The player that was originally controlling <see cref="Entity"/>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public NetUserId? Controller;
|
public NetUserId Controller;
|
||||||
|
|
||||||
public (EntityCoordinates Coords, Angle Rot)? Local;
|
public (EntityCoordinates Coords, Angle Rot)? Local;
|
||||||
public (EntityCoordinates Coords, Angle Rot)? World;
|
public (EntityCoordinates Coords, Angle Rot)? World;
|
||||||
@@ -35,27 +36,17 @@ public sealed partial class ReplaySpectatorSystem
|
|||||||
public SpectatorData GetSpectatorData()
|
public SpectatorData GetSpectatorData()
|
||||||
{
|
{
|
||||||
var data = new SpectatorData();
|
var data = new SpectatorData();
|
||||||
|
if (_player.LocalEntity is not { } player)
|
||||||
if (_player.LocalPlayer?.ControlledEntity is not { } player)
|
|
||||||
return data;
|
return data;
|
||||||
|
|
||||||
foreach (var session in _player.Sessions)
|
data.Controller = _player.LocalUser ?? DefaultUser;
|
||||||
{
|
|
||||||
if (session.UserId == _player.LocalPlayer?.UserId)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (session.AttachedEntity == player)
|
|
||||||
{
|
|
||||||
data.Controller = session.UserId;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!TryComp(player, out TransformComponent? xform) || xform.MapUid == null)
|
if (!TryComp(player, out TransformComponent? xform) || xform.MapUid == null)
|
||||||
return data;
|
return data;
|
||||||
|
|
||||||
data.Local = (xform.Coordinates, xform.LocalRotation);
|
data.Local = (xform.Coordinates, xform.LocalRotation);
|
||||||
data.World = (new(xform.MapUid.Value, xform.WorldPosition), xform.WorldRotation);
|
var (pos, rot) = _transform.GetWorldPositionRotation(player);
|
||||||
|
data.World = (new(xform.MapUid.Value, pos), rot);
|
||||||
|
|
||||||
if (TryComp(player, out InputMoverComponent? mover))
|
if (TryComp(player, out InputMoverComponent? mover))
|
||||||
data.Eye = (mover.RelativeEntity, mover.TargetRelativeRotation);
|
data.Eye = (mover.RelativeEntity, mover.TargetRelativeRotation);
|
||||||
@@ -77,18 +68,54 @@ public sealed partial class ReplaySpectatorSystem
|
|||||||
_spectatorData = null;
|
_spectatorData = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void OnBeforeApplyState((GameState Current, GameState? Next) args)
|
||||||
|
{
|
||||||
|
// Before applying the game state, we want to check to see if a recorded player session is about to
|
||||||
|
// get attached to the entity that we are currently spectating. If it is, then we switch out local session
|
||||||
|
// to the recorded session. I.e., we switch from spectating the entity to spectating the session.
|
||||||
|
// This is required because having multiple sessions attached to a single entity is not currently supported.
|
||||||
|
|
||||||
|
if (_player.LocalUser != DefaultUser)
|
||||||
|
return; // Already spectating some session.
|
||||||
|
|
||||||
|
if (_player.LocalEntity is not {} uid)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var netEnt = GetNetEntity(uid);
|
||||||
|
if (netEnt.IsClientSide())
|
||||||
|
return;
|
||||||
|
|
||||||
|
foreach (var playerState in args.Current.PlayerStates.Value)
|
||||||
|
{
|
||||||
|
if (playerState.ControlledEntity != netEnt)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (!_player.TryGetSessionById(playerState.UserId, out var session))
|
||||||
|
session = _player.CreateAndAddSession(playerState.UserId, playerState.Name);
|
||||||
|
|
||||||
|
_player.SetLocalSession(session);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void SetSpectatorPosition(SpectatorData data)
|
public void SetSpectatorPosition(SpectatorData data)
|
||||||
{
|
{
|
||||||
if (_player.LocalSession == null)
|
if (_player.LocalSession == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (data.Controller != null
|
if (data.Controller != DefaultUser)
|
||||||
&& _player.SessionsDict.TryGetValue(data.Controller.Value, out var session)
|
|
||||||
&& Exists(session.AttachedEntity)
|
|
||||||
&& Transform(session.AttachedEntity.Value).MapID != MapId.Nullspace)
|
|
||||||
{
|
{
|
||||||
_player.SetAttachedEntity(_player.LocalSession, session.AttachedEntity);
|
// the "local player" is currently set to some recorded session. As long as that session has an entity, we
|
||||||
return;
|
// do nothing here
|
||||||
|
if (_player.TryGetSessionById(data.Controller, out var session)
|
||||||
|
&& Exists(session.AttachedEntity))
|
||||||
|
{
|
||||||
|
_player.SetLocalSession(session);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Spectated session is no longer valid - return to the client-side session
|
||||||
|
_player.SetLocalSession(_player.GetSessionById(DefaultUser));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Exists(data.Entity) && Transform(data.Entity).MapID != MapId.Nullspace)
|
if (Exists(data.Entity) && Transform(data.Entity).MapID != MapId.Nullspace)
|
||||||
@@ -114,7 +141,7 @@ public sealed partial class ReplaySpectatorSystem
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
Logger.Error("Failed to find a suitable observer spawn point");
|
Log.Error("Failed to find a suitable observer spawn point");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,7 +180,7 @@ public sealed partial class ReplaySpectatorSystem
|
|||||||
|
|
||||||
private void OnTerminating(EntityUid uid, ReplaySpectatorComponent component, ref EntityTerminatingEvent args)
|
private void OnTerminating(EntityUid uid, ReplaySpectatorComponent component, ref EntityTerminatingEvent args)
|
||||||
{
|
{
|
||||||
if (uid != _player.LocalPlayer?.ControlledEntity)
|
if (uid != _player.LocalEntity)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var xform = Transform(uid);
|
var xform = Transform(uid);
|
||||||
@@ -165,7 +192,7 @@ public sealed partial class ReplaySpectatorSystem
|
|||||||
|
|
||||||
private void OnParentChanged(EntityUid uid, ReplaySpectatorComponent component, ref EntParentChangedMessage args)
|
private void OnParentChanged(EntityUid uid, ReplaySpectatorComponent component, ref EntParentChangedMessage args)
|
||||||
{
|
{
|
||||||
if (uid != _player.LocalPlayer?.ControlledEntity)
|
if (uid != _player.LocalEntity)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (args.Transform.MapUid != null || args.OldMapId == MapId.Nullspace)
|
if (args.Transform.MapUid != null || args.OldMapId == MapId.Nullspace)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using Content.Client.Replay.UI;
|
|||||||
using Content.Shared.Verbs;
|
using Content.Shared.Verbs;
|
||||||
using Robust.Shared.Console;
|
using Robust.Shared.Console;
|
||||||
using Robust.Shared.Map;
|
using Robust.Shared.Map;
|
||||||
|
using Robust.Shared.Player;
|
||||||
using Robust.Shared.Utility;
|
using Robust.Shared.Utility;
|
||||||
|
|
||||||
namespace Content.Client.Replay.Spectator;
|
namespace Content.Client.Replay.Spectator;
|
||||||
@@ -15,19 +16,13 @@ public sealed partial class ReplaySpectatorSystem
|
|||||||
if (_replayPlayback.Replay == null)
|
if (_replayPlayback.Replay == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var verb = new AlternativeVerb
|
ev.Verbs.Add(new AlternativeVerb
|
||||||
{
|
{
|
||||||
Priority = 100,
|
Priority = 100,
|
||||||
Act = () =>
|
Act = () => SpectateEntity(ev.Target),
|
||||||
{
|
|
||||||
SpectateEntity(ev.Target);
|
|
||||||
},
|
|
||||||
|
|
||||||
Text = Loc.GetString("replay-verb-spectate"),
|
Text = Loc.GetString("replay-verb-spectate"),
|
||||||
Icon = new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/VerbIcons/vv.svg.192dpi.png"))
|
Icon = new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/VerbIcons/vv.svg.192dpi.png"))
|
||||||
};
|
});
|
||||||
|
|
||||||
ev.Verbs.Add(verb);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SpectateEntity(EntityUid target)
|
public void SpectateEntity(EntityUid target)
|
||||||
@@ -35,7 +30,7 @@ public sealed partial class ReplaySpectatorSystem
|
|||||||
if (_player.LocalSession == null)
|
if (_player.LocalSession == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var old = _player.LocalSession.AttachedEntity;
|
var old = _player.LocalEntity;
|
||||||
|
|
||||||
if (old == target)
|
if (old == target)
|
||||||
{
|
{
|
||||||
@@ -44,8 +39,11 @@ public sealed partial class ReplaySpectatorSystem
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_player.SetAttachedEntity(_player.LocalSession, target);
|
|
||||||
EnsureComp<ReplaySpectatorComponent>(target);
|
EnsureComp<ReplaySpectatorComponent>(target);
|
||||||
|
if (TryComp(target, out ActorComponent? actor))
|
||||||
|
_player.SetLocalSession(actor.PlayerSession);
|
||||||
|
else
|
||||||
|
_player.SetAttachedEntity(_player.LocalSession, target);
|
||||||
|
|
||||||
_stateMan.RequestStateChange<ReplaySpectateEntityState>();
|
_stateMan.RequestStateChange<ReplaySpectateEntityState>();
|
||||||
if (old == null)
|
if (old == null)
|
||||||
@@ -59,10 +57,9 @@ public sealed partial class ReplaySpectatorSystem
|
|||||||
|
|
||||||
public TransformComponent SpawnSpectatorGhost(EntityCoordinates coords, bool gridAttach)
|
public TransformComponent SpawnSpectatorGhost(EntityCoordinates coords, bool gridAttach)
|
||||||
{
|
{
|
||||||
if (_player.LocalSession == null)
|
var old = _player.LocalEntity;
|
||||||
throw new InvalidOperationException();
|
var session = _player.GetSessionById(DefaultUser);
|
||||||
|
_player.SetLocalSession(session);
|
||||||
var old = _player.LocalSession.AttachedEntity;
|
|
||||||
|
|
||||||
var ent = Spawn("ReplayObserver", coords);
|
var ent = Spawn("ReplayObserver", coords);
|
||||||
_eye.SetMaxZoom(ent, Vector2.One * 5);
|
_eye.SetMaxZoom(ent, Vector2.One * 5);
|
||||||
@@ -73,7 +70,7 @@ public sealed partial class ReplaySpectatorSystem
|
|||||||
if (gridAttach)
|
if (gridAttach)
|
||||||
_transform.AttachToGridOrMap(ent);
|
_transform.AttachToGridOrMap(ent);
|
||||||
|
|
||||||
_player.SetAttachedEntity(_player.LocalSession, ent);
|
_player.SetAttachedEntity(session, ent);
|
||||||
|
|
||||||
if (old != null)
|
if (old != null)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
using Content.Shared.Movement.Systems;
|
using Content.Shared.Movement.Systems;
|
||||||
using Content.Shared.Verbs;
|
using Content.Shared.Verbs;
|
||||||
using Robust.Client;
|
|
||||||
using Robust.Client.GameObjects;
|
using Robust.Client.GameObjects;
|
||||||
using Robust.Client.Player;
|
using Robust.Client.Player;
|
||||||
using Robust.Client.Replays.Playback;
|
using Robust.Client.Replays.Playback;
|
||||||
using Robust.Client.State;
|
using Robust.Client.State;
|
||||||
using Robust.Shared.Console;
|
using Robust.Shared.Console;
|
||||||
|
using Robust.Shared.Network;
|
||||||
using Robust.Shared.Player;
|
using Robust.Shared.Player;
|
||||||
using Robust.Shared.Serialization.Markdown.Mapping;
|
using Robust.Shared.Serialization.Markdown.Mapping;
|
||||||
|
|
||||||
@@ -27,13 +27,17 @@ public sealed partial class ReplaySpectatorSystem : EntitySystem
|
|||||||
[Dependency] private readonly IStateManager _stateMan = default!;
|
[Dependency] private readonly IStateManager _stateMan = default!;
|
||||||
[Dependency] private readonly TransformSystem _transform = default!;
|
[Dependency] private readonly TransformSystem _transform = default!;
|
||||||
[Dependency] private readonly SharedMoverController _mover = default!;
|
[Dependency] private readonly SharedMoverController _mover = default!;
|
||||||
[Dependency] private readonly IBaseClient _client = default!;
|
|
||||||
[Dependency] private readonly SharedContentEyeSystem _eye = default!;
|
[Dependency] private readonly SharedContentEyeSystem _eye = default!;
|
||||||
[Dependency] private readonly IReplayPlaybackManager _replayPlayback = default!;
|
[Dependency] private readonly IReplayPlaybackManager _replayPlayback = default!;
|
||||||
|
|
||||||
private SpectatorData? _spectatorData;
|
private SpectatorData? _spectatorData;
|
||||||
public const string SpectateCmd = "replay_spectate";
|
public const string SpectateCmd = "replay_spectate";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// User Id that corresponds to the local user in a single-player game.
|
||||||
|
/// </summary>
|
||||||
|
public static readonly NetUserId DefaultUser = default;
|
||||||
|
|
||||||
public override void Initialize()
|
public override void Initialize()
|
||||||
{
|
{
|
||||||
base.Initialize();
|
base.Initialize();
|
||||||
@@ -49,6 +53,7 @@ public sealed partial class ReplaySpectatorSystem : EntitySystem
|
|||||||
_replayPlayback.AfterSetTick += OnAfterSetTick;
|
_replayPlayback.AfterSetTick += OnAfterSetTick;
|
||||||
_replayPlayback.ReplayPlaybackStarted += OnPlaybackStarted;
|
_replayPlayback.ReplayPlaybackStarted += OnPlaybackStarted;
|
||||||
_replayPlayback.ReplayPlaybackStopped += OnPlaybackStopped;
|
_replayPlayback.ReplayPlaybackStopped += OnPlaybackStopped;
|
||||||
|
_replayPlayback.BeforeApplyState += OnBeforeApplyState;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void Shutdown()
|
public override void Shutdown()
|
||||||
@@ -58,6 +63,7 @@ public sealed partial class ReplaySpectatorSystem : EntitySystem
|
|||||||
_replayPlayback.AfterSetTick -= OnAfterSetTick;
|
_replayPlayback.AfterSetTick -= OnAfterSetTick;
|
||||||
_replayPlayback.ReplayPlaybackStarted -= OnPlaybackStarted;
|
_replayPlayback.ReplayPlaybackStarted -= OnPlaybackStarted;
|
||||||
_replayPlayback.ReplayPlaybackStopped -= OnPlaybackStopped;
|
_replayPlayback.ReplayPlaybackStopped -= OnPlaybackStopped;
|
||||||
|
_replayPlayback.BeforeApplyState -= OnBeforeApplyState;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnPlaybackStarted(MappingDataNode yamlMappingNode, List<object> objects)
|
private void OnPlaybackStarted(MappingDataNode yamlMappingNode, List<object> objects)
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ public sealed class ReplayMainScreen : State
|
|||||||
_mainMenuControl.QuitButton.OnPressed += QuitButtonPressed;
|
_mainMenuControl.QuitButton.OnPressed += QuitButtonPressed;
|
||||||
_mainMenuControl.OptionsButton.OnPressed += OptionsButtonPressed;
|
_mainMenuControl.OptionsButton.OnPressed += OptionsButtonPressed;
|
||||||
_mainMenuControl.FolderButton.OnPressed += OnFolderPressed;
|
_mainMenuControl.FolderButton.OnPressed += OnFolderPressed;
|
||||||
_mainMenuControl.LoadButton.OnPressed += OnLoadpressed;
|
_mainMenuControl.LoadButton.OnPressed += OnLoadPressed;
|
||||||
|
|
||||||
_directory = new ResPath(_cfg.GetCVar(CVars.ReplayDirectory)).ToRootedPath();
|
_directory = new ResPath(_cfg.GetCVar(CVars.ReplayDirectory)).ToRootedPath();
|
||||||
RefreshReplays();
|
RefreshReplays();
|
||||||
@@ -205,7 +205,7 @@ public sealed class ReplayMainScreen : State
|
|||||||
_resMan.UserData.OpenOsWindow(_directory);
|
_resMan.UserData.OpenOsWindow(_directory);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnLoadpressed(BaseButton.ButtonEventArgs obj)
|
private void OnLoadPressed(BaseButton.ButtonEventArgs obj)
|
||||||
{
|
{
|
||||||
if (_selected.HasValue)
|
if (_selected.HasValue)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -647,7 +647,9 @@ public abstract class SharedActionsSystem : EntitySystem
|
|||||||
|
|
||||||
if (action.AttachedEntity != performer)
|
if (action.AttachedEntity != performer)
|
||||||
{
|
{
|
||||||
DebugTools.Assert(!Resolve(performer, ref comp, false) || !comp.Actions.Contains(actionId.Value));
|
DebugTools.Assert(!Resolve(performer, ref comp, false)
|
||||||
|
|| comp.LifeStage >= ComponentLifeStage.Stopping
|
||||||
|
|| !comp.Actions.Contains(actionId.Value));
|
||||||
|
|
||||||
if (!GameTiming.ApplyingState)
|
if (!GameTiming.ApplyingState)
|
||||||
Log.Error($"Attempted to remove an action {ToPrettyString(actionId)} from an entity that it was never attached to: {ToPrettyString(performer)}");
|
Log.Error($"Attempted to remove an action {ToPrettyString(actionId)} from an entity that it was never attached to: {ToPrettyString(performer)}");
|
||||||
|
|||||||
Reference in New Issue
Block a user