Fix observer command & button (#17151)
This commit is contained in:
@@ -57,7 +57,7 @@ public sealed partial class ReplaySpectatorSystem
|
||||
if (!player.IsClientSide() || !HasComp<ReplaySpectatorComponent>(player))
|
||||
{
|
||||
// Player is trying to move -> behave like the ghost-on-move component.
|
||||
SpawnObserverGhost(new EntityCoordinates(player, default), true);
|
||||
SpawnSpectatorGhost(new EntityCoordinates(player, default), true);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -80,7 +80,7 @@ public sealed partial class ReplaySpectatorSystem
|
||||
if (!xform.ParentUid.IsValid())
|
||||
{
|
||||
// Were they sitting on a grid as it was getting deleted?
|
||||
SetObserverPosition(default);
|
||||
SetSpectatorPosition(default);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
using System.Linq;
|
||||
using Content.Shared.Movement.Components;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Map.Components;
|
||||
|
||||
namespace Content.Client.Replay.Spectator;
|
||||
|
||||
// This partial class contains functions for getting and setting the spectator's position data, so that
|
||||
// a consistent view/camera can be maintained when jumping around in time.
|
||||
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
|
||||
{
|
||||
// TODO REPLAYS handle ghost-following.
|
||||
public EntityUid Entity;
|
||||
public (EntityCoordinates Coords, Angle Rot)? Local;
|
||||
public (EntityCoordinates Coords, Angle Rot)? World;
|
||||
public (EntityUid? Ent, Angle Rot)? Eye;
|
||||
}
|
||||
|
||||
public SpectatorPosition GetSpectatorPosition()
|
||||
{
|
||||
var obs = new SpectatorPosition();
|
||||
if (_player.LocalPlayer?.ControlledEntity is { } player && TryComp(player, out TransformComponent? xform) && xform.MapUid != null)
|
||||
{
|
||||
obs.Local = (xform.Coordinates, xform.LocalRotation);
|
||||
obs.World = (new(xform.MapUid.Value, xform.WorldPosition), xform.WorldRotation);
|
||||
|
||||
if (TryComp(player, out InputMoverComponent? mover))
|
||||
obs.Eye = (mover.RelativeEntity, mover.TargetRelativeRotation);
|
||||
|
||||
obs.Entity = player;
|
||||
}
|
||||
|
||||
return obs;
|
||||
}
|
||||
|
||||
private void OnBeforeSetTick()
|
||||
{
|
||||
_oldPosition = GetSpectatorPosition();
|
||||
}
|
||||
|
||||
private void OnAfterSetTick()
|
||||
{
|
||||
if (_oldPosition != null)
|
||||
SetSpectatorPosition(_oldPosition.Value);
|
||||
_oldPosition = null;
|
||||
}
|
||||
|
||||
public void SetSpectatorPosition(SpectatorPosition spectatorPosition)
|
||||
{
|
||||
if (Exists(spectatorPosition.Entity) && Transform(spectatorPosition.Entity).MapID != MapId.Nullspace)
|
||||
{
|
||||
_player.LocalPlayer!.AttachEntity(spectatorPosition.Entity, EntityManager, _client);
|
||||
return;
|
||||
}
|
||||
|
||||
if (spectatorPosition.Local != null && spectatorPosition.Local.Value.Coords.IsValid(EntityManager))
|
||||
{
|
||||
var newXform = SpawnSpectatorGhost(spectatorPosition.Local.Value.Coords, false);
|
||||
newXform.LocalRotation = spectatorPosition.Local.Value.Rot;
|
||||
}
|
||||
else if (spectatorPosition.World != null && spectatorPosition.World.Value.Coords.IsValid(EntityManager))
|
||||
{
|
||||
var newXform = SpawnSpectatorGhost(spectatorPosition.World.Value.Coords, true);
|
||||
newXform.LocalRotation = spectatorPosition.World.Value.Rot;
|
||||
}
|
||||
else if (TryFindFallbackSpawn(out var coords))
|
||||
{
|
||||
var newXform = SpawnSpectatorGhost(coords, true);
|
||||
newXform.LocalRotation = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.Error("Failed to find a suitable observer spawn point");
|
||||
return;
|
||||
}
|
||||
|
||||
if (spectatorPosition.Eye != null && TryComp(_player.LocalPlayer?.ControlledEntity, out InputMoverComponent? newMover))
|
||||
{
|
||||
newMover.RelativeEntity = spectatorPosition.Eye.Value.Ent;
|
||||
newMover.TargetRelativeRotation = newMover.RelativeRotation = spectatorPosition.Eye.Value.Rot;
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryFindFallbackSpawn(out EntityCoordinates coords)
|
||||
{
|
||||
var uid = EntityQuery<MapGridComponent>()
|
||||
.OrderByDescending(x => x.LocalAABB.Size.LengthSquared)
|
||||
.FirstOrDefault()?.Owner;
|
||||
coords = new EntityCoordinates(uid ?? default, default);
|
||||
return uid != null;
|
||||
}
|
||||
|
||||
private void OnTerminating(EntityUid uid, ReplaySpectatorComponent component, ref EntityTerminatingEvent args)
|
||||
{
|
||||
if (uid != _player.LocalPlayer?.ControlledEntity)
|
||||
return;
|
||||
|
||||
var xform = Transform(uid);
|
||||
if (xform.MapUid == null || Terminating(xform.MapUid.Value))
|
||||
return;
|
||||
|
||||
SpawnSpectatorGhost(new EntityCoordinates(xform.MapUid.Value, default), true);
|
||||
}
|
||||
|
||||
private void OnParentChanged(EntityUid uid, ReplaySpectatorComponent component, ref EntParentChangedMessage args)
|
||||
{
|
||||
if (uid != _player.LocalPlayer?.ControlledEntity)
|
||||
return;
|
||||
|
||||
if (args.Transform.MapUid != null || args.OldMapId == MapId.Nullspace)
|
||||
return;
|
||||
|
||||
// The entity being spectated from was moved to null-space.
|
||||
// This was probably because they were spectating some entity in a client-side replay that left PVS range.
|
||||
// Simple respawn the ghost.
|
||||
SetSpectatorPosition(default);
|
||||
}
|
||||
|
||||
private void OnDetached(EntityUid uid, ReplaySpectatorComponent component, PlayerDetachedEvent args)
|
||||
{
|
||||
if (uid.IsClientSide())
|
||||
QueueDel(uid);
|
||||
else
|
||||
RemCompDeferred(uid, component);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
using Content.Client.Replay.UI;
|
||||
using Content.Shared.Verbs;
|
||||
using Robust.Shared.Console;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Client.Replay.Spectator;
|
||||
|
||||
// This partial class has methods for spawning a spectator ghost and "possessing" entitites.
|
||||
public sealed partial class ReplaySpectatorSystem
|
||||
{
|
||||
private void OnGetAlternativeVerbs(GetVerbsEvent<AlternativeVerb> ev)
|
||||
{
|
||||
if (_replayPlayback.Replay == null)
|
||||
return;
|
||||
|
||||
var verb = new AlternativeVerb
|
||||
{
|
||||
Priority = 100,
|
||||
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)
|
||||
{
|
||||
if (_player.LocalPlayer == null)
|
||||
return;
|
||||
|
||||
var old = _player.LocalPlayer.ControlledEntity;
|
||||
|
||||
if (old == target)
|
||||
{
|
||||
// un-visit
|
||||
SpawnSpectatorGhost(Transform(target).Coordinates, true);
|
||||
return;
|
||||
}
|
||||
|
||||
_player.LocalPlayer.AttachEntity(target, EntityManager, _client);
|
||||
EnsureComp<ReplaySpectatorComponent>(target);
|
||||
|
||||
_stateMan.RequestStateChange<ReplaySpectateEntityState>();
|
||||
if (old == null)
|
||||
return;
|
||||
|
||||
if (old.Value.IsClientSide())
|
||||
Del(old.Value);
|
||||
else
|
||||
RemComp<ReplaySpectatorComponent>(old.Value);
|
||||
}
|
||||
|
||||
public TransformComponent SpawnSpectatorGhost(EntityCoordinates coords, bool gridAttach)
|
||||
{
|
||||
if (_player.LocalPlayer == null)
|
||||
throw new InvalidOperationException();
|
||||
|
||||
var old = _player.LocalPlayer.ControlledEntity;
|
||||
|
||||
var ent = Spawn("MobObserver", coords);
|
||||
_eye.SetMaxZoom(ent, Vector2.One * 5);
|
||||
EnsureComp<ReplaySpectatorComponent>(ent);
|
||||
|
||||
var xform = Transform(ent);
|
||||
|
||||
if (gridAttach)
|
||||
_transform.AttachToGridOrMap(ent);
|
||||
|
||||
_player.LocalPlayer.AttachEntity(ent, EntityManager, _client);
|
||||
|
||||
if (old != null)
|
||||
{
|
||||
if (old.Value.IsClientSide())
|
||||
QueueDel(old.Value);
|
||||
else
|
||||
RemComp<ReplaySpectatorComponent>(old.Value);
|
||||
}
|
||||
|
||||
_stateMan.RequestStateChange<ReplayGhostState>();
|
||||
|
||||
return xform;
|
||||
}
|
||||
|
||||
private void SpectateCommand(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
if (args.Length == 0)
|
||||
{
|
||||
if (_player.LocalPlayer?.ControlledEntity is { } current)
|
||||
SpawnSpectatorGhost(new EntityCoordinates(current, default), true);
|
||||
else
|
||||
SpawnSpectatorGhost(default, true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!EntityUid.TryParse(args[0], out var uid))
|
||||
{
|
||||
shell.WriteError(Loc.GetString("cmd-parse-failure-uid", ("arg", args[0])));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Exists(uid))
|
||||
{
|
||||
shell.WriteError(Loc.GetString("cmd-parse-failure-entity-exist", ("arg", args[0])));
|
||||
return;
|
||||
}
|
||||
|
||||
SpectateEntity(uid);
|
||||
}
|
||||
|
||||
private CompletionResult SpectateCompletions(IConsoleShell shell, string[] args)
|
||||
{
|
||||
if (args.Length != 1)
|
||||
return CompletionResult.Empty;
|
||||
|
||||
return CompletionResult.FromHintOptions(CompletionHelper.EntityUids(args[0],
|
||||
EntityManager), Loc.GetString("cmd-replay-spectate-hint"));
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,3 @@
|
||||
using System.Linq;
|
||||
using Content.Client.Replay.UI;
|
||||
using Content.Shared.Movement.Components;
|
||||
using Content.Shared.Movement.Systems;
|
||||
using Content.Shared.Verbs;
|
||||
using Robust.Client;
|
||||
@@ -9,9 +6,6 @@ using Robust.Client.Player;
|
||||
using Robust.Client.Replays.Playback;
|
||||
using Robust.Client.State;
|
||||
using Robust.Shared.Console;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Map.Components;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Client.Replay.Spectator;
|
||||
|
||||
@@ -35,7 +29,8 @@ public sealed partial class ReplaySpectatorSystem : EntitySystem
|
||||
[Dependency] private readonly SharedContentEyeSystem _eye = default!;
|
||||
[Dependency] private readonly IReplayPlaybackManager _replayPlayback = default!;
|
||||
|
||||
private ObserverData? _oldPosition;
|
||||
private SpectatorPosition? _oldPosition;
|
||||
public const string SpectateCmd = "replay_spectate";
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
@@ -44,9 +39,9 @@ public sealed partial class ReplaySpectatorSystem : EntitySystem
|
||||
SubscribeLocalEvent<GetVerbsEvent<AlternativeVerb>>(OnGetAlternativeVerbs);
|
||||
SubscribeLocalEvent<ReplaySpectatorComponent, EntityTerminatingEvent>(OnTerminating);
|
||||
SubscribeLocalEvent<ReplaySpectatorComponent, PlayerDetachedEvent>(OnDetached);
|
||||
SubscribeLocalEvent<ReplaySpectatorComponent, EntParentChangedMessage>(OnParentChanged);
|
||||
|
||||
InitializeBlockers();
|
||||
_conHost.RegisterCommand("observe", ObserveCommand);
|
||||
|
||||
_replayPlayback.BeforeSetTick += OnBeforeSetTick;
|
||||
_replayPlayback.AfterSetTick += OnAfterSetTick;
|
||||
@@ -54,228 +49,29 @@ public sealed partial class ReplaySpectatorSystem : EntitySystem
|
||||
_replayPlayback.ReplayPlaybackStopped += OnPlaybackStopped;
|
||||
}
|
||||
|
||||
private void OnPlaybackStarted()
|
||||
{
|
||||
InitializeMovement();
|
||||
SetObserverPosition(default);
|
||||
}
|
||||
|
||||
private void OnAfterSetTick()
|
||||
{
|
||||
if (_oldPosition != null)
|
||||
SetObserverPosition(_oldPosition.Value);
|
||||
_oldPosition = null;
|
||||
}
|
||||
|
||||
public override void Shutdown()
|
||||
{
|
||||
base.Shutdown();
|
||||
_conHost.UnregisterCommand("observe");
|
||||
_replayPlayback.BeforeSetTick -= OnBeforeSetTick;
|
||||
_replayPlayback.AfterSetTick -= OnAfterSetTick;
|
||||
_replayPlayback.ReplayPlaybackStarted -= OnPlaybackStarted;
|
||||
_replayPlayback.ReplayPlaybackStopped -= OnPlaybackStopped;
|
||||
}
|
||||
|
||||
private void OnPlaybackStarted()
|
||||
{
|
||||
InitializeMovement();
|
||||
SetSpectatorPosition(default);
|
||||
_conHost.RegisterCommand(SpectateCmd,
|
||||
Loc.GetString("cmd-replay-spectate-desc"),
|
||||
Loc.GetString("cmd-replay-spectate-help"),
|
||||
SpectateCommand,
|
||||
SpectateCompletions);
|
||||
}
|
||||
|
||||
private void OnPlaybackStopped()
|
||||
{
|
||||
ShutdownMovement();
|
||||
}
|
||||
|
||||
private void OnBeforeSetTick()
|
||||
{
|
||||
_oldPosition = GetObserverPosition();
|
||||
}
|
||||
|
||||
private void OnDetached(EntityUid uid, ReplaySpectatorComponent component, PlayerDetachedEvent args)
|
||||
{
|
||||
if (uid.IsClientSide())
|
||||
QueueDel(uid);
|
||||
else
|
||||
RemCompDeferred(uid, component);
|
||||
}
|
||||
|
||||
public void SetObserverPosition(ObserverData observer)
|
||||
{
|
||||
if (Exists(observer.Entity) && Transform(observer.Entity).MapID != MapId.Nullspace)
|
||||
{
|
||||
_player.LocalPlayer!.AttachEntity(observer.Entity, EntityManager, _client);
|
||||
return;
|
||||
}
|
||||
|
||||
if (observer.Local != null && observer.Local.Value.Coords.IsValid(EntityManager))
|
||||
{
|
||||
var newXform = SpawnObserverGhost(observer.Local.Value.Coords, false);
|
||||
newXform.LocalRotation = observer.Local.Value.Rot;
|
||||
}
|
||||
else if (observer.World != null && observer.World.Value.Coords.IsValid(EntityManager))
|
||||
{
|
||||
var newXform = SpawnObserverGhost(observer.World.Value.Coords, true);
|
||||
newXform.LocalRotation = observer.World.Value.Rot;
|
||||
}
|
||||
else if (TryFindFallbackSpawn(out var coords))
|
||||
{
|
||||
var newXform = SpawnObserverGhost(coords, true);
|
||||
newXform.LocalRotation = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.Error("Failed to find a suitable observer spawn point");
|
||||
return;
|
||||
}
|
||||
|
||||
if (observer.Eye != null && TryComp(_player.LocalPlayer?.ControlledEntity, out InputMoverComponent? newMover))
|
||||
{
|
||||
newMover.RelativeEntity = observer.Eye.Value.Ent;
|
||||
newMover.TargetRelativeRotation = newMover.RelativeRotation = observer.Eye.Value.Rot;
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryFindFallbackSpawn(out EntityCoordinates coords)
|
||||
{
|
||||
var uid = EntityQuery<MapGridComponent>().OrderByDescending(x => x.LocalAABB.Size.LengthSquared).FirstOrDefault()?.Owner;
|
||||
coords = new EntityCoordinates(uid ?? default, default);
|
||||
return uid != null;
|
||||
}
|
||||
|
||||
public struct ObserverData
|
||||
{
|
||||
// TODO REPLAYS handle ghost-following.
|
||||
public EntityUid Entity;
|
||||
public (EntityCoordinates Coords, Angle Rot)? Local;
|
||||
public (EntityCoordinates Coords, Angle Rot)? World;
|
||||
public (EntityUid? Ent, Angle Rot)? Eye;
|
||||
}
|
||||
|
||||
public ObserverData GetObserverPosition()
|
||||
{
|
||||
var obs = new ObserverData();
|
||||
if (_player.LocalPlayer?.ControlledEntity is { } player && TryComp(player, out TransformComponent? xform) && xform.MapUid != null)
|
||||
{
|
||||
obs.Local = (xform.Coordinates, xform.LocalRotation);
|
||||
obs.World = (new(xform.MapUid.Value, xform.WorldPosition), xform.WorldRotation);
|
||||
|
||||
if (TryComp(player, out InputMoverComponent? mover))
|
||||
obs.Eye = (mover.RelativeEntity, mover.TargetRelativeRotation);
|
||||
|
||||
obs.Entity = player;
|
||||
}
|
||||
|
||||
return obs;
|
||||
}
|
||||
|
||||
private void OnTerminating(EntityUid uid, ReplaySpectatorComponent component, ref EntityTerminatingEvent args)
|
||||
{
|
||||
if (uid != _player.LocalPlayer?.ControlledEntity)
|
||||
return;
|
||||
|
||||
var xform = Transform(uid);
|
||||
if (xform.MapUid == null || Terminating(xform.MapUid.Value))
|
||||
return;
|
||||
|
||||
SpawnObserverGhost(new EntityCoordinates(xform.MapUid.Value, default), true);
|
||||
}
|
||||
|
||||
private void OnGetAlternativeVerbs(GetVerbsEvent<AlternativeVerb> ev)
|
||||
{
|
||||
if (_replayPlayback.Replay == null)
|
||||
return;
|
||||
|
||||
var verb = new AlternativeVerb
|
||||
{
|
||||
Priority = 100,
|
||||
Act = () =>
|
||||
{
|
||||
SpectateEntity(ev.Target);
|
||||
},
|
||||
|
||||
Text = "Observe",
|
||||
Icon = new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/VerbIcons/vv.svg.192dpi.png"))
|
||||
};
|
||||
|
||||
ev.Verbs.Add(verb);
|
||||
}
|
||||
|
||||
public void SpectateEntity(EntityUid target)
|
||||
{
|
||||
if (_player.LocalPlayer == null)
|
||||
return;
|
||||
|
||||
var old = _player.LocalPlayer.ControlledEntity;
|
||||
|
||||
if (old == target)
|
||||
{
|
||||
// un-visit
|
||||
SpawnObserverGhost(Transform(target).Coordinates, true);
|
||||
return;
|
||||
}
|
||||
|
||||
_player.LocalPlayer.AttachEntity(target, EntityManager, _client);
|
||||
EnsureComp<ReplaySpectatorComponent>(target);
|
||||
|
||||
if (old == null)
|
||||
return;
|
||||
|
||||
if (old.Value.IsClientSide())
|
||||
Del(old.Value);
|
||||
else
|
||||
RemComp<ReplaySpectatorComponent>(old.Value);
|
||||
|
||||
_stateMan.RequestStateChange<ReplaySpectateEntityState>();
|
||||
}
|
||||
|
||||
public TransformComponent SpawnObserverGhost(EntityCoordinates coords, bool gridAttach)
|
||||
{
|
||||
if (_player.LocalPlayer == null)
|
||||
throw new InvalidOperationException();
|
||||
|
||||
var old = _player.LocalPlayer.ControlledEntity;
|
||||
|
||||
var ent = Spawn("MobObserver", coords);
|
||||
_eye.SetMaxZoom(ent, Vector2.One * 5);
|
||||
EnsureComp<ReplaySpectatorComponent>(ent);
|
||||
|
||||
var xform = Transform(ent);
|
||||
|
||||
if (gridAttach)
|
||||
_transform.AttachToGridOrMap(ent);
|
||||
|
||||
_player.LocalPlayer.AttachEntity(ent, EntityManager, _client);
|
||||
|
||||
if (old != null)
|
||||
{
|
||||
if (old.Value.IsClientSide())
|
||||
QueueDel(old.Value);
|
||||
else
|
||||
RemComp<ReplaySpectatorComponent>(old.Value);
|
||||
}
|
||||
|
||||
_stateMan.RequestStateChange<ReplayGhostState>();
|
||||
|
||||
return xform;
|
||||
}
|
||||
|
||||
private void ObserveCommand(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
if (args.Length == 0)
|
||||
{
|
||||
if (_player.LocalPlayer?.ControlledEntity is { } current)
|
||||
SpawnObserverGhost(new EntityCoordinates(current, default), true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!EntityUid.TryParse(args[0], out var uid))
|
||||
{
|
||||
shell.WriteError(Loc.GetString("cmd-parse-failure-uid", ("arg", args[0])));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!TryComp(uid, out TransformComponent? xform))
|
||||
{
|
||||
shell.WriteError(Loc.GetString("cmd-parse-failure-entity-exist", ("arg", args[0])));
|
||||
return;
|
||||
}
|
||||
|
||||
SpectateEntity(uid);
|
||||
_conHost.UnregisterCommand(SpectateCmd);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,3 +32,11 @@ replay-info-info = {"["}color=gray]Selected:[/color] {$name} ({$file})
|
||||
|
||||
# Replay selection window
|
||||
replay-menu-select-title = Select Replay
|
||||
|
||||
# Replay related verbs
|
||||
replay-verb-spectate = Spectate
|
||||
|
||||
# command
|
||||
cmd-replay-spectate-help = replay_spectate [optional entity]
|
||||
cmd-replay-spectate-desc = Attaches or detaches the local player to a given entity uid.
|
||||
cmd-replay-spectate-hint = Optional EntityUid
|
||||
|
||||
Reference in New Issue
Block a user