ReplaySpectatorSystem.Position.cs 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233
  1. using Content.Shared.Movement.Components;
  2. using Content.Shared.Station.Components;
  3. using Robust.Shared.GameStates;
  4. using Robust.Shared.Map;
  5. using Robust.Shared.Map.Components;
  6. using Robust.Shared.Network;
  7. using Robust.Shared.Player;
  8. namespace Content.Client.Replay.Spectator;
  9. // This partial class contains functions for getting and setting the spectator's position data, so that
  10. // a consistent view/camera can be maintained when jumping around in time.
  11. public sealed partial class ReplaySpectatorSystem
  12. {
  13. /// <summary>
  14. /// Simple struct containing position & rotation data for maintaining a persistent view when jumping around in time.
  15. /// </summary>
  16. public struct SpectatorData
  17. {
  18. // TODO REPLAYS handle ghost-following.
  19. /// <summary>
  20. /// The current entity being spectated.
  21. /// </summary>
  22. public EntityUid Entity;
  23. /// <summary>
  24. /// The player that was originally controlling <see cref="Entity"/>
  25. /// </summary>
  26. public NetUserId Controller;
  27. public (EntityCoordinates Coords, Angle Rot)? Local;
  28. public (EntityCoordinates Coords, Angle Rot)? World;
  29. public (EntityUid? Ent, Angle Rot)? Eye;
  30. }
  31. public SpectatorData GetSpectatorData()
  32. {
  33. var data = new SpectatorData();
  34. if (_player.LocalEntity is not { } player)
  35. return data;
  36. data.Controller = _player.LocalUser ?? DefaultUser;
  37. if (!TryComp(player, out TransformComponent? xform) || xform.MapUid == null)
  38. return data;
  39. data.Local = (xform.Coordinates, xform.LocalRotation);
  40. var (pos, rot) = _transform.GetWorldPositionRotation(player);
  41. data.World = (new(xform.MapUid.Value, pos), rot);
  42. if (TryComp(player, out InputMoverComponent? mover))
  43. data.Eye = (mover.RelativeEntity, mover.TargetRelativeRotation);
  44. data.Entity = player;
  45. return data;
  46. }
  47. private void OnBeforeSetTick()
  48. {
  49. _spectatorData = GetSpectatorData();
  50. }
  51. private void OnAfterSetTick()
  52. {
  53. if (_spectatorData != null)
  54. SetSpectatorPosition(_spectatorData.Value);
  55. _spectatorData = null;
  56. }
  57. private void OnBeforeApplyState((GameState Current, GameState? Next) args)
  58. {
  59. // Before applying the game state, we want to check to see if a recorded player session is about to
  60. // get attached to the entity that we are currently spectating. If it is, then we switch out local session
  61. // to the recorded session. I.e., we switch from spectating the entity to spectating the session.
  62. // This is required because having multiple sessions attached to a single entity is not currently supported.
  63. if (_player.LocalUser != DefaultUser)
  64. return; // Already spectating some session.
  65. if (_player.LocalEntity is not {} uid)
  66. return;
  67. var netEnt = GetNetEntity(uid);
  68. if (netEnt.IsClientSide())
  69. return;
  70. foreach (var playerState in args.Current.PlayerStates.Value)
  71. {
  72. if (playerState.ControlledEntity != netEnt)
  73. continue;
  74. if (!_player.TryGetSessionById(playerState.UserId, out var session))
  75. session = _player.CreateAndAddSession(playerState.UserId, playerState.Name);
  76. _player.SetLocalSession(session);
  77. break;
  78. }
  79. }
  80. public void SetSpectatorPosition(SpectatorData data)
  81. {
  82. if (_player.LocalSession == null)
  83. return;
  84. if (data.Controller != DefaultUser)
  85. {
  86. // the "local player" is currently set to some recorded session. As long as that session has an entity, we
  87. // do nothing here
  88. if (_player.TryGetSessionById(data.Controller, out var session)
  89. && Exists(session.AttachedEntity))
  90. {
  91. _player.SetLocalSession(session);
  92. return;
  93. }
  94. // Spectated session is no longer valid - return to the client-side session
  95. _player.SetLocalSession(_player.GetSessionById(DefaultUser));
  96. }
  97. if (Exists(data.Entity) && Transform(data.Entity).MapID != MapId.Nullspace)
  98. {
  99. _player.SetAttachedEntity(_player.LocalSession, data.Entity);
  100. return;
  101. }
  102. if (data.Local != null && data.Local.Value.Coords.IsValid(EntityManager))
  103. {
  104. var newXform = SpawnSpectatorGhost(data.Local.Value.Coords, false);
  105. newXform.LocalRotation = data.Local.Value.Rot;
  106. }
  107. else if (data.World != null && data.World.Value.Coords.IsValid(EntityManager))
  108. {
  109. var newXform = SpawnSpectatorGhost(data.World.Value.Coords, true);
  110. newXform.LocalRotation = data.World.Value.Rot;
  111. }
  112. else if (TryFindFallbackSpawn(out var coords))
  113. {
  114. var newXform = SpawnSpectatorGhost(coords, true);
  115. newXform.LocalRotation = 0;
  116. }
  117. else
  118. {
  119. Log.Error("Failed to find a suitable observer spawn point");
  120. return;
  121. }
  122. if (data.Eye != null && TryComp(_player.LocalSession.AttachedEntity, out InputMoverComponent? newMover))
  123. {
  124. newMover.RelativeEntity = data.Eye.Value.Ent;
  125. newMover.TargetRelativeRotation = newMover.RelativeRotation = data.Eye.Value.Rot;
  126. }
  127. }
  128. private bool TryFindFallbackSpawn(out EntityCoordinates coords)
  129. {
  130. if (_replayPlayback.TryGetRecorderEntity(out var recorder))
  131. {
  132. coords = new EntityCoordinates(recorder.Value, default);
  133. return true;
  134. }
  135. Entity<MapGridComponent>? maxUid = null;
  136. float? maxSize = null;
  137. var gridQuery = EntityQueryEnumerator<MapGridComponent>();
  138. var stationFound = false;
  139. while (gridQuery.MoveNext(out var uid, out var grid))
  140. {
  141. var size = grid.LocalAABB.Size.LengthSquared();
  142. var station = HasComp<StationMemberComponent>(uid);
  143. //We want the first station grid to overwrite any previous non-station grids no matter the size, in case the vgroid was found first
  144. if (maxSize is not null && size < maxSize && !(!stationFound && station))
  145. continue;
  146. if (!station && stationFound)
  147. continue;
  148. maxUid = (uid, grid);
  149. maxSize = size;
  150. if (station)
  151. stationFound = true;
  152. }
  153. coords = new EntityCoordinates(maxUid ?? default, default);
  154. return maxUid != null;
  155. }
  156. private void OnTerminating(EntityUid uid, ReplaySpectatorComponent component, ref EntityTerminatingEvent args)
  157. {
  158. if (uid != _player.LocalEntity)
  159. return;
  160. var xform = Transform(uid);
  161. if (xform.MapUid == null || Terminating(xform.MapUid.Value))
  162. return;
  163. SpawnSpectatorGhost(new EntityCoordinates(xform.MapUid.Value, default), true);
  164. }
  165. private void OnParentChanged(EntityUid uid, ReplaySpectatorComponent component, ref EntParentChangedMessage args)
  166. {
  167. if (uid != _player.LocalEntity)
  168. return;
  169. if (args.Transform.MapUid != null || args.OldMapId == null)
  170. return;
  171. if (_spectatorData != null)
  172. {
  173. // Currently scrubbing/setting the replay tick
  174. // the observer will get respawned once the state was applied
  175. return;
  176. }
  177. // The entity being spectated from was moved to null-space.
  178. // This was probably because they were spectating some entity in a client-side replay that left PVS range.
  179. // Simple respawn the ghost.
  180. SetSpectatorPosition(default);
  181. }
  182. private void OnDetached(EntityUid uid, ReplaySpectatorComponent component, LocalPlayerDetachedEvent args)
  183. {
  184. if (IsClientSide(uid))
  185. QueueDel(uid);
  186. else
  187. RemCompDeferred(uid, component);
  188. }
  189. }