PointingSystem.cs 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319
  1. using System.Linq;
  2. using Content.Server.Administration.Logs;
  3. using Content.Server.Pointing.Components;
  4. using Content.Shared.CCVar;
  5. using Content.Shared.Database;
  6. using Content.Shared.Examine;
  7. using Content.Shared.Eye;
  8. using Content.Shared.Ghost;
  9. using Content.Shared.IdentityManagement;
  10. using Content.Shared.Input;
  11. using Content.Shared.Interaction;
  12. using Content.Shared.Mind;
  13. using Content.Shared.Pointing;
  14. using Content.Shared.Popups;
  15. using JetBrains.Annotations;
  16. using Robust.Server.GameObjects;
  17. using Robust.Server.Player;
  18. using Robust.Shared.Configuration;
  19. using Robust.Shared.Enums;
  20. using Robust.Shared.GameStates;
  21. using Robust.Shared.Input.Binding;
  22. using Robust.Shared.Map;
  23. using Robust.Shared.Player;
  24. using Robust.Shared.Replays;
  25. using Robust.Shared.Timing;
  26. namespace Content.Server.Pointing.EntitySystems
  27. {
  28. [UsedImplicitly]
  29. internal sealed class PointingSystem : SharedPointingSystem
  30. {
  31. [Dependency] private readonly IConfigurationManager _config = default!;
  32. [Dependency] private readonly IReplayRecordingManager _replay = default!;
  33. [Dependency] private readonly IMapManager _mapManager = default!;
  34. [Dependency] private readonly IPlayerManager _playerManager = default!;
  35. [Dependency] private readonly ITileDefinitionManager _tileDefinitionManager = default!;
  36. [Dependency] private readonly IGameTiming _gameTiming = default!;
  37. [Dependency] private readonly RotateToFaceSystem _rotateToFaceSystem = default!;
  38. [Dependency] private readonly SharedPopupSystem _popup = default!;
  39. [Dependency] private readonly VisibilitySystem _visibilitySystem = default!;
  40. [Dependency] private readonly SharedMindSystem _minds = default!;
  41. [Dependency] private readonly SharedTransformSystem _transform = default!;
  42. [Dependency] private readonly SharedMapSystem _map = default!;
  43. [Dependency] private readonly IAdminLogManager _adminLogger = default!;
  44. [Dependency] private readonly ExamineSystemShared _examine = default!;
  45. private TimeSpan _pointDelay = TimeSpan.FromSeconds(0.5f);
  46. /// <summary>
  47. /// A dictionary of players to the last time that they
  48. /// pointed at something.
  49. /// </summary>
  50. private readonly Dictionary<ICommonSession, TimeSpan> _pointers = new();
  51. private const float PointingRange = 15f;
  52. private void GetCompState(Entity<PointingArrowComponent> entity, ref ComponentGetState args)
  53. {
  54. args.State = new SharedPointingArrowComponentState
  55. {
  56. StartPosition = entity.Comp.StartPosition,
  57. EndTime = entity.Comp.EndTime
  58. };
  59. }
  60. private void OnPlayerStatusChanged(object? sender, SessionStatusEventArgs e)
  61. {
  62. if (e.NewStatus != SessionStatus.Disconnected)
  63. {
  64. return;
  65. }
  66. _pointers.Remove(e.Session);
  67. }
  68. // TODO: FOV
  69. private void SendMessage(
  70. EntityUid source,
  71. IEnumerable<ICommonSession> viewers,
  72. EntityUid pointed,
  73. string selfMessage,
  74. string viewerMessage,
  75. string? viewerPointedAtMessage = null)
  76. {
  77. var netSource = GetNetEntity(source);
  78. foreach (var viewer in viewers)
  79. {
  80. if (viewer.AttachedEntity is not {Valid: true} viewerEntity)
  81. {
  82. continue;
  83. }
  84. var message = viewerEntity == source
  85. ? selfMessage
  86. : viewerEntity == pointed && viewerPointedAtMessage != null
  87. ? viewerPointedAtMessage
  88. : viewerMessage;
  89. // Someone pointing at YOU is slightly more important
  90. var popupType = viewerEntity == pointed ? PopupType.Medium : PopupType.Small;
  91. RaiseNetworkEvent(new PopupEntityEvent(message, popupType, netSource), viewerEntity);
  92. }
  93. _replay.RecordServerMessage(new PopupEntityEvent(viewerMessage, PopupType.Small, netSource));
  94. }
  95. public bool InRange(EntityUid pointer, EntityCoordinates coordinates)
  96. {
  97. if (HasComp<GhostComponent>(pointer))
  98. {
  99. return _transform.InRange(Transform(pointer).Coordinates, coordinates, 15);
  100. }
  101. else
  102. {
  103. return _examine.InRangeUnOccluded(pointer, coordinates, 15, predicate: e => e == pointer);
  104. }
  105. }
  106. public bool TryPoint(ICommonSession? session, EntityCoordinates coordsPointed, EntityUid pointed)
  107. {
  108. if (session?.AttachedEntity is not { } player)
  109. {
  110. Log.Warning($"Player {session} attempted to point without any attached entity");
  111. return false;
  112. }
  113. if (!coordsPointed.IsValid(EntityManager))
  114. {
  115. Log.Warning($"Player {ToPrettyString(player)} attempted to point at invalid coordinates: {coordsPointed}");
  116. return false;
  117. }
  118. if (_pointers.TryGetValue(session, out var lastTime) &&
  119. _gameTiming.CurTime < lastTime + _pointDelay)
  120. {
  121. return false;
  122. }
  123. if (HasComp<PointingArrowComponent>(pointed))
  124. {
  125. // this is a pointing arrow. no pointing here...
  126. return false;
  127. }
  128. if (!CanPoint(player))
  129. {
  130. return false;
  131. }
  132. if (!InRange(player, coordsPointed))
  133. {
  134. _popup.PopupEntity(Loc.GetString("pointing-system-try-point-cannot-reach"), player, player);
  135. return false;
  136. }
  137. var mapCoordsPointed = _transform.ToMapCoordinates(coordsPointed);
  138. _rotateToFaceSystem.TryFaceCoordinates(player, mapCoordsPointed.Position);
  139. var arrow = EntityManager.SpawnEntity("PointingArrow", coordsPointed);
  140. if (TryComp<PointingArrowComponent>(arrow, out var pointing))
  141. {
  142. pointing.StartPosition = _transform.ToCoordinates((arrow, Transform(arrow)), _transform.ToMapCoordinates(Transform(player).Coordinates)).Position;
  143. pointing.EndTime = _gameTiming.CurTime + PointDuration;
  144. Dirty(arrow, pointing);
  145. }
  146. if (EntityQuery<PointingArrowAngeringComponent>().FirstOrDefault() != null)
  147. {
  148. if (TryComp<PointingArrowComponent>(arrow, out var pointingArrowComponent))
  149. {
  150. pointingArrowComponent.Rogue = true;
  151. }
  152. }
  153. var layer = (int) VisibilityFlags.Normal;
  154. if (TryComp(player, out VisibilityComponent? playerVisibility))
  155. {
  156. var arrowVisibility = EntityManager.EnsureComponent<VisibilityComponent>(arrow);
  157. layer = playerVisibility.Layer;
  158. _visibilitySystem.SetLayer((arrow, arrowVisibility), (ushort) layer);
  159. }
  160. // Get players that are in range and whose visibility layer matches the arrow's.
  161. bool ViewerPredicate(ICommonSession playerSession)
  162. {
  163. if (!_minds.TryGetMind(playerSession, out _, out var mind) ||
  164. mind.CurrentEntity is not { Valid: true } ent ||
  165. !TryComp(ent, out EyeComponent? eyeComp) ||
  166. (eyeComp.VisibilityMask & layer) == 0)
  167. return false;
  168. return _transform.GetMapCoordinates(ent).InRange(_transform.GetMapCoordinates(player), PointingRange);
  169. }
  170. var viewers = Filter.Empty()
  171. .AddWhere(session1 => ViewerPredicate(session1))
  172. .Recipients;
  173. string selfMessage;
  174. string viewerMessage;
  175. string? viewerPointedAtMessage = null;
  176. var playerName = Identity.Entity(player, EntityManager);
  177. if (Exists(pointed))
  178. {
  179. var pointedName = Identity.Entity(pointed, EntityManager);
  180. selfMessage = player == pointed
  181. ? Loc.GetString("pointing-system-point-at-self")
  182. : Loc.GetString("pointing-system-point-at-other", ("other", pointedName));
  183. viewerMessage = player == pointed
  184. ? Loc.GetString("pointing-system-point-at-self-others", ("otherName", playerName), ("other", playerName))
  185. : Loc.GetString("pointing-system-point-at-other-others", ("otherName", playerName), ("other", pointedName));
  186. viewerPointedAtMessage = Loc.GetString("pointing-system-point-at-you-other", ("otherName", playerName));
  187. var ev = new AfterPointedAtEvent(pointed);
  188. RaiseLocalEvent(player, ref ev);
  189. var gotev = new AfterGotPointedAtEvent(player);
  190. RaiseLocalEvent(pointed, ref gotev);
  191. _adminLogger.Add(LogType.Action, LogImpact.Low, $"{ToPrettyString(player):user} pointed at {ToPrettyString(pointed):target} {Transform(pointed).Coordinates}");
  192. }
  193. else
  194. {
  195. TileRef? tileRef = null;
  196. string? position = null;
  197. if (_mapManager.TryFindGridAt(mapCoordsPointed, out var gridUid, out var grid))
  198. {
  199. position = $"EntId={gridUid} {_map.WorldToTile(gridUid, grid, mapCoordsPointed.Position)}";
  200. tileRef = _map.GetTileRef(gridUid, grid, _map.WorldToTile(gridUid, grid, mapCoordsPointed.Position));
  201. }
  202. var tileDef = _tileDefinitionManager[tileRef?.Tile.TypeId ?? 0];
  203. var name = Loc.GetString(tileDef.Name);
  204. selfMessage = Loc.GetString("pointing-system-point-at-tile", ("tileName", name));
  205. viewerMessage = Loc.GetString("pointing-system-other-point-at-tile", ("otherName", playerName), ("tileName", name));
  206. _adminLogger.Add(LogType.Action, LogImpact.Low, $"{ToPrettyString(player):user} pointed at {name} {(position == null ? mapCoordsPointed : position)}");
  207. }
  208. _pointers[session] = _gameTiming.CurTime;
  209. SendMessage(player, viewers, pointed, selfMessage, viewerMessage, viewerPointedAtMessage);
  210. return true;
  211. }
  212. public override void Initialize()
  213. {
  214. base.Initialize();
  215. SubscribeLocalEvent<PointingArrowComponent, ComponentGetState>(GetCompState);
  216. SubscribeNetworkEvent<PointingAttemptEvent>(OnPointAttempt);
  217. _playerManager.PlayerStatusChanged += OnPlayerStatusChanged;
  218. CommandBinds.Builder
  219. .Bind(ContentKeyFunctions.Point, new PointerInputCmdHandler(TryPoint))
  220. .Register<PointingSystem>();
  221. Subs.CVar(_config, CCVars.PointingCooldownSeconds, v => _pointDelay = TimeSpan.FromSeconds(v), true);
  222. }
  223. private void OnPointAttempt(PointingAttemptEvent ev, EntitySessionEventArgs args)
  224. {
  225. var target = GetEntity(ev.Target);
  226. if (TryComp(target, out TransformComponent? xformTarget))
  227. TryPoint(args.SenderSession, xformTarget.Coordinates, target);
  228. else
  229. Log.Warning($"User {args.SenderSession} attempted to point at a non-existent entity uid: {ev.Target}");
  230. }
  231. public override void Shutdown()
  232. {
  233. base.Shutdown();
  234. _playerManager.PlayerStatusChanged -= OnPlayerStatusChanged;
  235. _pointers.Clear();
  236. }
  237. public override void Update(float frameTime)
  238. {
  239. var currentTime = _gameTiming.CurTime;
  240. var query = AllEntityQuery<PointingArrowComponent>();
  241. while (query.MoveNext(out var uid, out var component))
  242. {
  243. Update((uid, component), currentTime);
  244. }
  245. }
  246. private void Update(Entity<PointingArrowComponent> pointing, TimeSpan currentTime)
  247. {
  248. // TODO: That pause PR
  249. var component = pointing.Comp;
  250. if (component.EndTime > currentTime)
  251. return;
  252. if (component.Rogue)
  253. {
  254. RemComp<PointingArrowComponent>(pointing);
  255. EnsureComp<RoguePointingArrowComponent>(pointing);
  256. return;
  257. }
  258. Del(pointing);
  259. }
  260. }
  261. }