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 IClientAdminManager _adminMan = default!;
|
||||
[Dependency] private readonly IPlayerManager _player = default!;
|
||||
[Dependency] private readonly IBaseClient _client = default!;
|
||||
|
||||
/// <summary>
|
||||
/// UI state to return to when stopping a replay or loading fails.
|
||||
@@ -87,6 +88,9 @@ public sealed class ContentReplayPlaybackManager
|
||||
_stateMan.RequestStateChange<LauncherConnecting>().SetDisconnected();
|
||||
else
|
||||
_stateMan.RequestStateChange<MainScreen>();
|
||||
|
||||
if (_client.RunLevel == ClientRunLevel.SinglePlayerGame)
|
||||
_client.StopSinglePlayer();
|
||||
}
|
||||
|
||||
private void OnCheckpointReset()
|
||||
|
||||
@@ -44,7 +44,7 @@ public sealed partial class ReplaySpectatorSystem
|
||||
if (_replayPlayback.Replay == null)
|
||||
return;
|
||||
|
||||
if (_player.LocalPlayer?.ControlledEntity is not { } player)
|
||||
if (_player.LocalEntity is not { } player)
|
||||
return;
|
||||
|
||||
if (Direction == DirectionFlag.None)
|
||||
@@ -99,7 +99,7 @@ public sealed partial class ReplaySpectatorSystem
|
||||
var worldVec = parentRotation.RotateVec(localVec);
|
||||
var speed = CompOrNull<MovementSpeedModifierComponent>(player)?.BaseSprintSpeed ?? DefaultSpeed;
|
||||
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
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Content.Shared.Movement.Components;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Map.Components;
|
||||
using Robust.Shared.Network;
|
||||
@@ -25,7 +26,7 @@ public sealed partial class ReplaySpectatorSystem
|
||||
/// <summary>
|
||||
/// The player that was originally controlling <see cref="Entity"/>
|
||||
/// </summary>
|
||||
public NetUserId? Controller;
|
||||
public NetUserId Controller;
|
||||
|
||||
public (EntityCoordinates Coords, Angle Rot)? Local;
|
||||
public (EntityCoordinates Coords, Angle Rot)? World;
|
||||
@@ -35,27 +36,17 @@ public sealed partial class ReplaySpectatorSystem
|
||||
public SpectatorData GetSpectatorData()
|
||||
{
|
||||
var data = new SpectatorData();
|
||||
|
||||
if (_player.LocalPlayer?.ControlledEntity is not { } player)
|
||||
if (_player.LocalEntity is not { } player)
|
||||
return data;
|
||||
|
||||
foreach (var session in _player.Sessions)
|
||||
{
|
||||
if (session.UserId == _player.LocalPlayer?.UserId)
|
||||
continue;
|
||||
|
||||
if (session.AttachedEntity == player)
|
||||
{
|
||||
data.Controller = session.UserId;
|
||||
break;
|
||||
}
|
||||
}
|
||||
data.Controller = _player.LocalUser ?? DefaultUser;
|
||||
|
||||
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);
|
||||
var (pos, rot) = _transform.GetWorldPositionRotation(player);
|
||||
data.World = (new(xform.MapUid.Value, pos), rot);
|
||||
|
||||
if (TryComp(player, out InputMoverComponent? mover))
|
||||
data.Eye = (mover.RelativeEntity, mover.TargetRelativeRotation);
|
||||
@@ -77,20 +68,56 @@ public sealed partial class ReplaySpectatorSystem
|
||||
_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)
|
||||
{
|
||||
if (_player.LocalSession == 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)
|
||||
if (data.Controller != DefaultUser)
|
||||
{
|
||||
_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
|
||||
// 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)
|
||||
{
|
||||
_player.SetAttachedEntity(_player.LocalSession, data.Entity);
|
||||
@@ -114,7 +141,7 @@ public sealed partial class ReplaySpectatorSystem
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.Error("Failed to find a suitable observer spawn point");
|
||||
Log.Error("Failed to find a suitable observer spawn point");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -153,7 +180,7 @@ public sealed partial class ReplaySpectatorSystem
|
||||
|
||||
private void OnTerminating(EntityUid uid, ReplaySpectatorComponent component, ref EntityTerminatingEvent args)
|
||||
{
|
||||
if (uid != _player.LocalPlayer?.ControlledEntity)
|
||||
if (uid != _player.LocalEntity)
|
||||
return;
|
||||
|
||||
var xform = Transform(uid);
|
||||
@@ -165,7 +192,7 @@ public sealed partial class ReplaySpectatorSystem
|
||||
|
||||
private void OnParentChanged(EntityUid uid, ReplaySpectatorComponent component, ref EntParentChangedMessage args)
|
||||
{
|
||||
if (uid != _player.LocalPlayer?.ControlledEntity)
|
||||
if (uid != _player.LocalEntity)
|
||||
return;
|
||||
|
||||
if (args.Transform.MapUid != null || args.OldMapId == MapId.Nullspace)
|
||||
|
||||
@@ -3,6 +3,7 @@ using Content.Client.Replay.UI;
|
||||
using Content.Shared.Verbs;
|
||||
using Robust.Shared.Console;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Client.Replay.Spectator;
|
||||
@@ -15,19 +16,13 @@ public sealed partial class ReplaySpectatorSystem
|
||||
if (_replayPlayback.Replay == null)
|
||||
return;
|
||||
|
||||
var verb = new AlternativeVerb
|
||||
ev.Verbs.Add(new AlternativeVerb
|
||||
{
|
||||
Priority = 100,
|
||||
Act = () =>
|
||||
{
|
||||
SpectateEntity(ev.Target);
|
||||
},
|
||||
|
||||
Act = () => SpectateEntity(ev.Target),
|
||||
Text = Loc.GetString("replay-verb-spectate"),
|
||||
Icon = new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/VerbIcons/vv.svg.192dpi.png"))
|
||||
};
|
||||
|
||||
ev.Verbs.Add(verb);
|
||||
});
|
||||
}
|
||||
|
||||
public void SpectateEntity(EntityUid target)
|
||||
@@ -35,7 +30,7 @@ public sealed partial class ReplaySpectatorSystem
|
||||
if (_player.LocalSession == null)
|
||||
return;
|
||||
|
||||
var old = _player.LocalSession.AttachedEntity;
|
||||
var old = _player.LocalEntity;
|
||||
|
||||
if (old == target)
|
||||
{
|
||||
@@ -44,8 +39,11 @@ public sealed partial class ReplaySpectatorSystem
|
||||
return;
|
||||
}
|
||||
|
||||
_player.SetAttachedEntity(_player.LocalSession, target);
|
||||
EnsureComp<ReplaySpectatorComponent>(target);
|
||||
if (TryComp(target, out ActorComponent? actor))
|
||||
_player.SetLocalSession(actor.PlayerSession);
|
||||
else
|
||||
_player.SetAttachedEntity(_player.LocalSession, target);
|
||||
|
||||
_stateMan.RequestStateChange<ReplaySpectateEntityState>();
|
||||
if (old == null)
|
||||
@@ -59,10 +57,9 @@ public sealed partial class ReplaySpectatorSystem
|
||||
|
||||
public TransformComponent SpawnSpectatorGhost(EntityCoordinates coords, bool gridAttach)
|
||||
{
|
||||
if (_player.LocalSession == null)
|
||||
throw new InvalidOperationException();
|
||||
|
||||
var old = _player.LocalSession.AttachedEntity;
|
||||
var old = _player.LocalEntity;
|
||||
var session = _player.GetSessionById(DefaultUser);
|
||||
_player.SetLocalSession(session);
|
||||
|
||||
var ent = Spawn("ReplayObserver", coords);
|
||||
_eye.SetMaxZoom(ent, Vector2.One * 5);
|
||||
@@ -73,7 +70,7 @@ public sealed partial class ReplaySpectatorSystem
|
||||
if (gridAttach)
|
||||
_transform.AttachToGridOrMap(ent);
|
||||
|
||||
_player.SetAttachedEntity(_player.LocalSession, ent);
|
||||
_player.SetAttachedEntity(session, ent);
|
||||
|
||||
if (old != null)
|
||||
{
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
using Content.Shared.Movement.Systems;
|
||||
using Content.Shared.Verbs;
|
||||
using Robust.Client;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.Player;
|
||||
using Robust.Client.Replays.Playback;
|
||||
using Robust.Client.State;
|
||||
using Robust.Shared.Console;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Player;
|
||||
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 TransformSystem _transform = default!;
|
||||
[Dependency] private readonly SharedMoverController _mover = default!;
|
||||
[Dependency] private readonly IBaseClient _client = default!;
|
||||
[Dependency] private readonly SharedContentEyeSystem _eye = default!;
|
||||
[Dependency] private readonly IReplayPlaybackManager _replayPlayback = default!;
|
||||
|
||||
private SpectatorData? _spectatorData;
|
||||
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()
|
||||
{
|
||||
base.Initialize();
|
||||
@@ -49,6 +53,7 @@ public sealed partial class ReplaySpectatorSystem : EntitySystem
|
||||
_replayPlayback.AfterSetTick += OnAfterSetTick;
|
||||
_replayPlayback.ReplayPlaybackStarted += OnPlaybackStarted;
|
||||
_replayPlayback.ReplayPlaybackStopped += OnPlaybackStopped;
|
||||
_replayPlayback.BeforeApplyState += OnBeforeApplyState;
|
||||
}
|
||||
|
||||
public override void Shutdown()
|
||||
@@ -58,6 +63,7 @@ public sealed partial class ReplaySpectatorSystem : EntitySystem
|
||||
_replayPlayback.AfterSetTick -= OnAfterSetTick;
|
||||
_replayPlayback.ReplayPlaybackStarted -= OnPlaybackStarted;
|
||||
_replayPlayback.ReplayPlaybackStopped -= OnPlaybackStopped;
|
||||
_replayPlayback.BeforeApplyState -= OnBeforeApplyState;
|
||||
}
|
||||
|
||||
private void OnPlaybackStarted(MappingDataNode yamlMappingNode, List<object> objects)
|
||||
|
||||
@@ -47,7 +47,7 @@ public sealed class ReplayMainScreen : State
|
||||
_mainMenuControl.QuitButton.OnPressed += QuitButtonPressed;
|
||||
_mainMenuControl.OptionsButton.OnPressed += OptionsButtonPressed;
|
||||
_mainMenuControl.FolderButton.OnPressed += OnFolderPressed;
|
||||
_mainMenuControl.LoadButton.OnPressed += OnLoadpressed;
|
||||
_mainMenuControl.LoadButton.OnPressed += OnLoadPressed;
|
||||
|
||||
_directory = new ResPath(_cfg.GetCVar(CVars.ReplayDirectory)).ToRootedPath();
|
||||
RefreshReplays();
|
||||
@@ -205,7 +205,7 @@ public sealed class ReplayMainScreen : State
|
||||
_resMan.UserData.OpenOsWindow(_directory);
|
||||
}
|
||||
|
||||
private void OnLoadpressed(BaseButton.ButtonEventArgs obj)
|
||||
private void OnLoadPressed(BaseButton.ButtonEventArgs obj)
|
||||
{
|
||||
if (_selected.HasValue)
|
||||
{
|
||||
|
||||
@@ -647,7 +647,9 @@ public abstract class SharedActionsSystem : EntitySystem
|
||||
|
||||
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)
|
||||
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