diff --git a/Content.Client/Replay/ContentReplayPlaybackManager.cs b/Content.Client/Replay/ContentReplayPlaybackManager.cs index cbb5117255..2b4e8ddc7d 100644 --- a/Content.Client/Replay/ContentReplayPlaybackManager.cs +++ b/Content.Client/Replay/ContentReplayPlaybackManager.cs @@ -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!; /// /// UI state to return to when stopping a replay or loading fails. @@ -87,6 +88,9 @@ public sealed class ContentReplayPlaybackManager _stateMan.RequestStateChange().SetDisconnected(); else _stateMan.RequestStateChange(); + + if (_client.RunLevel == ClientRunLevel.SinglePlayerGame) + _client.StopSinglePlayer(); } private void OnCheckpointReset() diff --git a/Content.Client/Replay/Spectator/ReplaySpectatorSystem.Movement.cs b/Content.Client/Replay/Spectator/ReplaySpectatorSystem.Movement.cs index d48a1eab46..e7d01713e5 100644 --- a/Content.Client/Replay/Spectator/ReplaySpectatorSystem.Movement.cs +++ b/Content.Client/Replay/Spectator/ReplaySpectatorSystem.Movement.cs @@ -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(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 diff --git a/Content.Client/Replay/Spectator/ReplaySpectatorSystem.Position.cs b/Content.Client/Replay/Spectator/ReplaySpectatorSystem.Position.cs index 6041d87317..2ee7e30ec9 100644 --- a/Content.Client/Replay/Spectator/ReplaySpectatorSystem.Position.cs +++ b/Content.Client/Replay/Spectator/ReplaySpectatorSystem.Position.cs @@ -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 /// /// The player that was originally controlling /// - 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,18 +68,54 @@ 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); - return; + // 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) @@ -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) diff --git a/Content.Client/Replay/Spectator/ReplaySpectatorSystem.Spectate.cs b/Content.Client/Replay/Spectator/ReplaySpectatorSystem.Spectate.cs index 80a8429055..27b33b84c5 100644 --- a/Content.Client/Replay/Spectator/ReplaySpectatorSystem.Spectate.cs +++ b/Content.Client/Replay/Spectator/ReplaySpectatorSystem.Spectate.cs @@ -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(target); + if (TryComp(target, out ActorComponent? actor)) + _player.SetLocalSession(actor.PlayerSession); + else + _player.SetAttachedEntity(_player.LocalSession, target); _stateMan.RequestStateChange(); 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) { diff --git a/Content.Client/Replay/Spectator/ReplaySpectatorSystem.cs b/Content.Client/Replay/Spectator/ReplaySpectatorSystem.cs index c238f5456d..8a3b858720 100644 --- a/Content.Client/Replay/Spectator/ReplaySpectatorSystem.cs +++ b/Content.Client/Replay/Spectator/ReplaySpectatorSystem.cs @@ -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"; + /// + /// User Id that corresponds to the local user in a single-player game. + /// + 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 objects) diff --git a/Content.Replay/Menu/ReplayMainMenu.cs b/Content.Replay/Menu/ReplayMainMenu.cs index 5792c1bc01..8bd99f82fb 100644 --- a/Content.Replay/Menu/ReplayMainMenu.cs +++ b/Content.Replay/Menu/ReplayMainMenu.cs @@ -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) { diff --git a/Content.Shared/Actions/SharedActionsSystem.cs b/Content.Shared/Actions/SharedActionsSystem.cs index 666110575a..46ef2058ad 100644 --- a/Content.Shared/Actions/SharedActionsSystem.cs +++ b/Content.Shared/Actions/SharedActionsSystem.cs @@ -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)}");