1
0

HolopadSystem.cs 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756
  1. using Content.Server.Chat.Systems;
  2. using Content.Server.Popups;
  3. using Content.Server.Power.EntitySystems;
  4. using Content.Server.Speech.Components;
  5. using Content.Server.Telephone;
  6. using Content.Shared.Access.Systems;
  7. using Content.Shared.Audio;
  8. using Content.Shared.Chat.TypingIndicator;
  9. using Content.Shared.Holopad;
  10. using Content.Shared.IdentityManagement;
  11. using Content.Shared.Labels.Components;
  12. using Content.Shared.Silicons.StationAi;
  13. using Content.Shared.Speech;
  14. using Content.Shared.Telephone;
  15. using Content.Shared.UserInterface;
  16. using Content.Shared.Verbs;
  17. using Robust.Server.GameObjects;
  18. using Robust.Server.GameStates;
  19. using Robust.Shared.Containers;
  20. using Robust.Shared.Timing;
  21. using Robust.Shared.Utility;
  22. using System.Linq;
  23. namespace Content.Server.Holopad;
  24. public sealed class HolopadSystem : SharedHolopadSystem
  25. {
  26. [Dependency] private readonly TelephoneSystem _telephoneSystem = default!;
  27. [Dependency] private readonly UserInterfaceSystem _userInterfaceSystem = default!;
  28. [Dependency] private readonly TransformSystem _xformSystem = default!;
  29. [Dependency] private readonly AppearanceSystem _appearanceSystem = default!;
  30. [Dependency] private readonly SharedPointLightSystem _pointLightSystem = default!;
  31. [Dependency] private readonly SharedAmbientSoundSystem _ambientSoundSystem = default!;
  32. [Dependency] private readonly SharedStationAiSystem _stationAiSystem = default!;
  33. [Dependency] private readonly AccessReaderSystem _accessReaderSystem = default!;
  34. [Dependency] private readonly ChatSystem _chatSystem = default!;
  35. [Dependency] private readonly PopupSystem _popupSystem = default!;
  36. [Dependency] private readonly IGameTiming _timing = default!;
  37. [Dependency] private readonly PvsOverrideSystem _pvs = default!;
  38. private float _updateTimer = 1.0f;
  39. private const float UpdateTime = 1.0f;
  40. public override void Initialize()
  41. {
  42. base.Initialize();
  43. // Holopad UI and bound user interface messages
  44. SubscribeLocalEvent<HolopadComponent, BeforeActivatableUIOpenEvent>(OnUIOpen);
  45. SubscribeLocalEvent<HolopadComponent, HolopadStartNewCallMessage>(OnHolopadStartNewCall);
  46. SubscribeLocalEvent<HolopadComponent, HolopadAnswerCallMessage>(OnHolopadAnswerCall);
  47. SubscribeLocalEvent<HolopadComponent, HolopadEndCallMessage>(OnHolopadEndCall);
  48. SubscribeLocalEvent<HolopadComponent, HolopadActivateProjectorMessage>(OnHolopadActivateProjector);
  49. SubscribeLocalEvent<HolopadComponent, HolopadStartBroadcastMessage>(OnHolopadStartBroadcast);
  50. SubscribeLocalEvent<HolopadComponent, HolopadStationAiRequestMessage>(OnHolopadStationAiRequest);
  51. // Holopad telephone events
  52. SubscribeLocalEvent<HolopadComponent, TelephoneStateChangeEvent>(OnTelephoneStateChange);
  53. SubscribeLocalEvent<HolopadComponent, TelephoneCallCommencedEvent>(OnHoloCallCommenced);
  54. SubscribeLocalEvent<HolopadComponent, TelephoneCallEndedEvent>(OnHoloCallEnded);
  55. SubscribeLocalEvent<HolopadComponent, TelephoneMessageSentEvent>(OnTelephoneMessageSent);
  56. // Networked events
  57. SubscribeNetworkEvent<HolopadUserTypingChangedEvent>(OnTypingChanged);
  58. // Component start/shutdown events
  59. SubscribeLocalEvent<HolopadComponent, ComponentInit>(OnHolopadInit);
  60. SubscribeLocalEvent<HolopadComponent, ComponentShutdown>(OnHolopadShutdown);
  61. SubscribeLocalEvent<HolopadUserComponent, ComponentInit>(OnHolopadUserInit);
  62. SubscribeLocalEvent<HolopadUserComponent, ComponentShutdown>(OnHolopadUserShutdown);
  63. // Misc events
  64. SubscribeLocalEvent<HolopadUserComponent, EmoteEvent>(OnEmote);
  65. SubscribeLocalEvent<HolopadUserComponent, JumpToCoreEvent>(OnJumpToCore);
  66. SubscribeLocalEvent<HolopadComponent, GetVerbsEvent<AlternativeVerb>>(AddToggleProjectorVerb);
  67. SubscribeLocalEvent<HolopadComponent, EntRemovedFromContainerMessage>(OnAiRemove);
  68. }
  69. #region: Holopad UI bound user interface messages
  70. private void OnUIOpen(Entity<HolopadComponent> entity, ref BeforeActivatableUIOpenEvent args)
  71. {
  72. UpdateUIState(entity);
  73. }
  74. private void OnHolopadStartNewCall(Entity<HolopadComponent> source, ref HolopadStartNewCallMessage args)
  75. {
  76. if (IsHolopadControlLocked(source, args.Actor))
  77. return;
  78. if (!TryComp<TelephoneComponent>(source, out var sourceTelephone))
  79. return;
  80. var receiver = GetEntity(args.Receiver);
  81. if (!TryComp<TelephoneComponent>(receiver, out var receiverTelephone))
  82. return;
  83. LinkHolopadToUser(source, args.Actor);
  84. _telephoneSystem.CallTelephone((source, sourceTelephone), (receiver, receiverTelephone), args.Actor);
  85. }
  86. private void OnHolopadAnswerCall(Entity<HolopadComponent> receiver, ref HolopadAnswerCallMessage args)
  87. {
  88. if (IsHolopadControlLocked(receiver, args.Actor))
  89. return;
  90. if (!TryComp<TelephoneComponent>(receiver, out var receiverTelephone))
  91. return;
  92. if (TryComp<StationAiHeldComponent>(args.Actor, out var userAiHeld))
  93. {
  94. var source = GetLinkedHolopads(receiver).FirstOrNull();
  95. if (source != null)
  96. {
  97. // Close any AI request windows
  98. if (_stationAiSystem.TryGetCore(args.Actor, out var stationAiCore))
  99. _userInterfaceSystem.CloseUi(receiver.Owner, HolopadUiKey.AiRequestWindow, args.Actor);
  100. // Try to warn the AI if the source of the call is out of its range
  101. if (TryComp<TelephoneComponent>(stationAiCore, out var stationAiTelephone) &&
  102. TryComp<TelephoneComponent>(source, out var sourceTelephone) &&
  103. !_telephoneSystem.IsSourceInRangeOfReceiver((stationAiCore.Owner, stationAiTelephone), (source.Value.Owner, sourceTelephone)))
  104. {
  105. _popupSystem.PopupEntity(Loc.GetString("holopad-ai-is-unable-to-reach-holopad"), receiver, args.Actor);
  106. return;
  107. }
  108. ActivateProjector(source.Value, args.Actor);
  109. }
  110. return;
  111. }
  112. LinkHolopadToUser(receiver, args.Actor);
  113. _telephoneSystem.AnswerTelephone((receiver, receiverTelephone), args.Actor);
  114. }
  115. private void OnHolopadEndCall(Entity<HolopadComponent> entity, ref HolopadEndCallMessage args)
  116. {
  117. if (!TryComp<TelephoneComponent>(entity, out var entityTelephone))
  118. return;
  119. if (IsHolopadControlLocked(entity, args.Actor))
  120. return;
  121. _telephoneSystem.EndTelephoneCalls((entity, entityTelephone));
  122. // If the user is an AI, end all calls originating from its
  123. // associated core to ensure that any broadcasts will end
  124. if (!TryComp<StationAiHeldComponent>(args.Actor, out var stationAiHeld) ||
  125. !_stationAiSystem.TryGetCore(args.Actor, out var stationAiCore))
  126. return;
  127. if (TryComp<TelephoneComponent>(stationAiCore, out var telephone))
  128. _telephoneSystem.EndTelephoneCalls((stationAiCore, telephone));
  129. }
  130. private void OnHolopadActivateProjector(Entity<HolopadComponent> entity, ref HolopadActivateProjectorMessage args)
  131. {
  132. ActivateProjector(entity, args.Actor);
  133. }
  134. private void OnHolopadStartBroadcast(Entity<HolopadComponent> source, ref HolopadStartBroadcastMessage args)
  135. {
  136. if (IsHolopadControlLocked(source, args.Actor) || IsHolopadBroadcastOnCoolDown(source))
  137. return;
  138. if (!_accessReaderSystem.IsAllowed(args.Actor, source))
  139. return;
  140. // AI broadcasting
  141. if (TryComp<StationAiHeldComponent>(args.Actor, out var stationAiHeld))
  142. {
  143. // Link the AI to the holopad they are broadcasting from
  144. LinkHolopadToUser(source, args.Actor);
  145. if (!_stationAiSystem.TryGetCore(args.Actor, out var stationAiCore) ||
  146. stationAiCore.Comp?.RemoteEntity == null ||
  147. !TryComp<HolopadComponent>(stationAiCore, out var stationAiCoreHolopad))
  148. return;
  149. // Execute the broadcast, but have it originate from the AI core
  150. ExecuteBroadcast((stationAiCore, stationAiCoreHolopad), args.Actor);
  151. // Switch the AI's perspective from free roaming to the target holopad
  152. _xformSystem.SetCoordinates(stationAiCore.Comp.RemoteEntity.Value, Transform(source).Coordinates);
  153. _stationAiSystem.SwitchRemoteEntityMode(stationAiCore, false);
  154. return;
  155. }
  156. // Crew broadcasting
  157. ExecuteBroadcast(source, args.Actor);
  158. }
  159. private void OnHolopadStationAiRequest(Entity<HolopadComponent> entity, ref HolopadStationAiRequestMessage args)
  160. {
  161. if (IsHolopadControlLocked(entity, args.Actor))
  162. return;
  163. if (!TryComp<TelephoneComponent>(entity, out var telephone))
  164. return;
  165. var source = new Entity<TelephoneComponent>(entity, telephone);
  166. var query = AllEntityQuery<StationAiCoreComponent, TelephoneComponent>();
  167. var reachableAiCores = new HashSet<Entity<TelephoneComponent>>();
  168. while (query.MoveNext(out var receiverUid, out var receiverStationAiCore, out var receiverTelephone))
  169. {
  170. var receiver = new Entity<TelephoneComponent>(receiverUid, receiverTelephone);
  171. // Check if the core can reach the call source, rather than the other way around
  172. if (!_telephoneSystem.IsSourceAbleToReachReceiver(receiver, source))
  173. continue;
  174. if (_telephoneSystem.IsTelephoneEngaged(receiver))
  175. continue;
  176. reachableAiCores.Add((receiverUid, receiverTelephone));
  177. if (!_stationAiSystem.TryGetHeld((receiver, receiverStationAiCore), out var insertedAi))
  178. continue;
  179. if (_userInterfaceSystem.TryOpenUi(receiverUid, HolopadUiKey.AiRequestWindow, insertedAi))
  180. LinkHolopadToUser(entity, args.Actor);
  181. }
  182. // Ignore range so that holopads that ignore other devices on the same grid can request the AI
  183. var options = new TelephoneCallOptions { IgnoreRange = true };
  184. _telephoneSystem.BroadcastCallToTelephones(source, reachableAiCores, args.Actor, options);
  185. }
  186. #endregion
  187. #region: Holopad telephone events
  188. private void OnTelephoneStateChange(Entity<HolopadComponent> holopad, ref TelephoneStateChangeEvent args)
  189. {
  190. // Update holopad visual and ambient states
  191. switch (args.NewState)
  192. {
  193. case TelephoneState.Idle:
  194. ShutDownHolopad(holopad);
  195. SetHolopadAmbientState(holopad, false);
  196. break;
  197. case TelephoneState.EndingCall:
  198. ShutDownHolopad(holopad);
  199. break;
  200. default:
  201. SetHolopadAmbientState(holopad, this.IsPowered(holopad, EntityManager));
  202. break;
  203. }
  204. }
  205. private void OnHoloCallCommenced(Entity<HolopadComponent> source, ref TelephoneCallCommencedEvent args)
  206. {
  207. if (source.Comp.Hologram == null)
  208. GenerateHologram(source);
  209. if (TryComp<HolopadComponent>(args.Receiver, out var receivingHolopad) && receivingHolopad.Hologram == null)
  210. GenerateHologram((args.Receiver, receivingHolopad));
  211. // Re-link the user to refresh the sprite data
  212. LinkHolopadToUser(source, source.Comp.User);
  213. }
  214. private void OnHoloCallEnded(Entity<HolopadComponent> entity, ref TelephoneCallEndedEvent args)
  215. {
  216. if (!TryComp<StationAiCoreComponent>(entity, out var stationAiCore))
  217. return;
  218. // Auto-close the AI request window
  219. if (_stationAiSystem.TryGetHeld((entity, stationAiCore), out var insertedAi))
  220. _userInterfaceSystem.CloseUi(entity.Owner, HolopadUiKey.AiRequestWindow, insertedAi);
  221. }
  222. private void OnTelephoneMessageSent(Entity<HolopadComponent> holopad, ref TelephoneMessageSentEvent args)
  223. {
  224. LinkHolopadToUser(holopad, args.MessageSource);
  225. }
  226. #endregion
  227. #region: Networked events
  228. private void OnTypingChanged(HolopadUserTypingChangedEvent ev, EntitySessionEventArgs args)
  229. {
  230. var uid = args.SenderSession.AttachedEntity;
  231. if (!Exists(uid))
  232. return;
  233. if (!TryComp<HolopadUserComponent>(uid, out var holopadUser))
  234. return;
  235. foreach (var linkedHolopad in holopadUser.LinkedHolopads)
  236. {
  237. var receiverHolopads = GetLinkedHolopads(linkedHolopad);
  238. foreach (var receiverHolopad in receiverHolopads)
  239. {
  240. if (receiverHolopad.Comp.Hologram == null)
  241. continue;
  242. _appearanceSystem.SetData(receiverHolopad.Comp.Hologram.Value.Owner, TypingIndicatorVisuals.IsTyping, ev.IsTyping);
  243. }
  244. }
  245. }
  246. #endregion
  247. #region: Component start/shutdown events
  248. private void OnHolopadInit(Entity<HolopadComponent> entity, ref ComponentInit args)
  249. {
  250. if (entity.Comp.User != null)
  251. LinkHolopadToUser(entity, entity.Comp.User.Value);
  252. }
  253. private void OnHolopadUserInit(Entity<HolopadUserComponent> entity, ref ComponentInit args)
  254. {
  255. foreach (var linkedHolopad in entity.Comp.LinkedHolopads)
  256. LinkHolopadToUser(linkedHolopad, entity);
  257. }
  258. private void OnHolopadShutdown(Entity<HolopadComponent> entity, ref ComponentShutdown args)
  259. {
  260. if (TryComp<TelephoneComponent>(entity, out var telphone) && _telephoneSystem.IsTelephoneEngaged((entity.Owner, telphone)))
  261. _telephoneSystem.EndTelephoneCalls((entity, telphone));
  262. ShutDownHolopad(entity);
  263. SetHolopadAmbientState(entity, false);
  264. }
  265. private void OnHolopadUserShutdown(Entity<HolopadUserComponent> entity, ref ComponentShutdown args)
  266. {
  267. foreach (var linkedHolopad in entity.Comp.LinkedHolopads)
  268. UnlinkHolopadFromUser(linkedHolopad, entity);
  269. }
  270. #endregion
  271. #region: Misc events
  272. private void OnEmote(Entity<HolopadUserComponent> entity, ref EmoteEvent args)
  273. {
  274. foreach (var linkedHolopad in entity.Comp.LinkedHolopads)
  275. {
  276. // Treat the ability to hear speech as the ability to also perceive emotes
  277. // (these are almost always going to be linked)
  278. if (!HasComp<ActiveListenerComponent>(linkedHolopad))
  279. continue;
  280. if (TryComp<TelephoneComponent>(linkedHolopad, out var linkedHolopadTelephone) && linkedHolopadTelephone.Muted)
  281. continue;
  282. var receivingHolopads = GetLinkedHolopads(linkedHolopad);
  283. var range = receivingHolopads.Count > 1 ? ChatTransmitRange.HideChat : ChatTransmitRange.GhostRangeLimit;
  284. foreach (var receiver in receivingHolopads)
  285. {
  286. if (receiver.Comp.Hologram == null)
  287. continue;
  288. // Name is based on the physical identity of the user
  289. var ent = Identity.Entity(entity, EntityManager);
  290. var name = Loc.GetString("holopad-hologram-name", ("name", ent));
  291. // Force the emote, because if the user can do it, the hologram can too
  292. _chatSystem.TryEmoteWithChat(receiver.Comp.Hologram.Value, args.Emote, range, false, name, true, true);
  293. }
  294. }
  295. }
  296. private void OnJumpToCore(Entity<HolopadUserComponent> entity, ref JumpToCoreEvent args)
  297. {
  298. if (!TryComp<StationAiHeldComponent>(entity, out var entityStationAiHeld))
  299. return;
  300. if (!_stationAiSystem.TryGetCore(entity, out var stationAiCore))
  301. return;
  302. if (!TryComp<TelephoneComponent>(stationAiCore, out var stationAiCoreTelephone))
  303. return;
  304. _telephoneSystem.EndTelephoneCalls((stationAiCore, stationAiCoreTelephone));
  305. }
  306. private void AddToggleProjectorVerb(Entity<HolopadComponent> entity, ref GetVerbsEvent<AlternativeVerb> args)
  307. {
  308. if (!args.CanAccess || !args.CanInteract)
  309. return;
  310. if (!this.IsPowered(entity, EntityManager))
  311. return;
  312. if (!TryComp<TelephoneComponent>(entity, out var entityTelephone) ||
  313. _telephoneSystem.IsTelephoneEngaged((entity, entityTelephone)))
  314. return;
  315. var user = args.User;
  316. if (!TryComp<StationAiHeldComponent>(user, out var userAiHeld))
  317. return;
  318. if (!_stationAiSystem.TryGetCore(user, out var stationAiCore) ||
  319. stationAiCore.Comp?.RemoteEntity == null)
  320. return;
  321. AlternativeVerb verb = new()
  322. {
  323. Act = () => ActivateProjector(entity, user),
  324. Text = Loc.GetString("holopad-activate-projector-verb"),
  325. Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/vv.svg.192dpi.png")),
  326. };
  327. args.Verbs.Add(verb);
  328. }
  329. private void OnAiRemove(Entity<HolopadComponent> entity, ref EntRemovedFromContainerMessage args)
  330. {
  331. if (!HasComp<StationAiCoreComponent>(entity))
  332. return;
  333. if (!TryComp<TelephoneComponent>(entity, out var entityTelephone))
  334. return;
  335. _telephoneSystem.EndTelephoneCalls((entity, entityTelephone));
  336. }
  337. #endregion
  338. public override void Update(float frameTime)
  339. {
  340. base.Update(frameTime);
  341. _updateTimer += frameTime;
  342. if (_updateTimer >= UpdateTime)
  343. {
  344. _updateTimer -= UpdateTime;
  345. var query = AllEntityQuery<HolopadComponent, TelephoneComponent, TransformComponent>();
  346. while (query.MoveNext(out var uid, out var holopad, out var telephone, out var xform))
  347. {
  348. UpdateUIState((uid, holopad), telephone);
  349. if (holopad.User != null &&
  350. !HasComp<IgnoreUIRangeComponent>(holopad.User) &&
  351. !_xformSystem.InRange((holopad.User.Value, Transform(holopad.User.Value)), (uid, xform), telephone.ListeningRange))
  352. {
  353. UnlinkHolopadFromUser((uid, holopad), holopad.User.Value);
  354. }
  355. }
  356. }
  357. }
  358. public void UpdateUIState(Entity<HolopadComponent> entity, TelephoneComponent? telephone = null)
  359. {
  360. if (!Resolve(entity.Owner, ref telephone, false))
  361. return;
  362. var source = new Entity<TelephoneComponent>(entity, telephone);
  363. var holopads = new Dictionary<NetEntity, string>();
  364. var query = AllEntityQuery<HolopadComponent, TelephoneComponent>();
  365. while (query.MoveNext(out var receiverUid, out var _, out var receiverTelephone))
  366. {
  367. var receiver = new Entity<TelephoneComponent>(receiverUid, receiverTelephone);
  368. if (receiverTelephone.UnlistedNumber)
  369. continue;
  370. if (source == receiver)
  371. continue;
  372. if (!_telephoneSystem.IsSourceInRangeOfReceiver(source, receiver))
  373. continue;
  374. var name = MetaData(receiverUid).EntityName;
  375. if (TryComp<LabelComponent>(receiverUid, out var label) && !string.IsNullOrEmpty(label.CurrentLabel))
  376. name = label.CurrentLabel;
  377. holopads.Add(GetNetEntity(receiverUid), name);
  378. }
  379. var uiKey = HasComp<StationAiCoreComponent>(entity) ? HolopadUiKey.AiActionWindow : HolopadUiKey.InteractionWindow;
  380. _userInterfaceSystem.SetUiState(entity.Owner, uiKey, new HolopadBoundInterfaceState(holopads));
  381. }
  382. private void GenerateHologram(Entity<HolopadComponent> entity)
  383. {
  384. if (entity.Comp.Hologram != null ||
  385. entity.Comp.HologramProtoId == null)
  386. return;
  387. var hologramUid = Spawn(entity.Comp.HologramProtoId, Transform(entity).Coordinates);
  388. // Safeguard - spawned holograms must have this component
  389. if (!TryComp<HolopadHologramComponent>(hologramUid, out var holopadHologram))
  390. {
  391. Del(hologramUid);
  392. return;
  393. }
  394. entity.Comp.Hologram = new Entity<HolopadHologramComponent>(hologramUid, holopadHologram);
  395. // Relay speech preferentially through the hologram
  396. if (TryComp<SpeechComponent>(hologramUid, out var hologramSpeech) &&
  397. TryComp<TelephoneComponent>(entity, out var entityTelephone))
  398. {
  399. _telephoneSystem.SetSpeakerForTelephone((entity, entityTelephone), (hologramUid, hologramSpeech));
  400. }
  401. }
  402. private void DeleteHologram(Entity<HolopadHologramComponent> hologram, Entity<HolopadComponent> attachedHolopad)
  403. {
  404. attachedHolopad.Comp.Hologram = null;
  405. QueueDel(hologram);
  406. }
  407. private void LinkHolopadToUser(Entity<HolopadComponent> entity, EntityUid? user)
  408. {
  409. if (user == null)
  410. {
  411. UnlinkHolopadFromUser(entity, null);
  412. return;
  413. }
  414. if (!TryComp<HolopadUserComponent>(user, out var holopadUser))
  415. holopadUser = AddComp<HolopadUserComponent>(user.Value);
  416. if (user != entity.Comp.User?.Owner)
  417. {
  418. // Removes the old user from the holopad
  419. UnlinkHolopadFromUser(entity, entity.Comp.User);
  420. // Assigns the new user in their place
  421. holopadUser.LinkedHolopads.Add(entity);
  422. entity.Comp.User = (user.Value, holopadUser);
  423. }
  424. // Add the new user to PVS and sync their appearance with any
  425. // holopads connected to the one they are using
  426. _pvs.AddGlobalOverride(user.Value);
  427. SyncHolopadHologramAppearanceWithTarget(entity, entity.Comp.User);
  428. }
  429. private void UnlinkHolopadFromUser(Entity<HolopadComponent> entity, Entity<HolopadUserComponent>? user)
  430. {
  431. entity.Comp.User = null;
  432. SyncHolopadHologramAppearanceWithTarget(entity, null);
  433. if (user == null)
  434. return;
  435. user.Value.Comp.LinkedHolopads.Remove(entity);
  436. if (!user.Value.Comp.LinkedHolopads.Any() &&
  437. user.Value.Comp.LifeStage < ComponentLifeStage.Stopping)
  438. {
  439. _pvs.RemoveGlobalOverride(user.Value);
  440. RemComp<HolopadUserComponent>(user.Value);
  441. }
  442. }
  443. private void SyncHolopadHologramAppearanceWithTarget(Entity<HolopadComponent> entity, Entity<HolopadUserComponent>? user)
  444. {
  445. foreach (var linkedHolopad in GetLinkedHolopads(entity))
  446. {
  447. if (linkedHolopad.Comp.Hologram == null)
  448. continue;
  449. if (user == null)
  450. _appearanceSystem.SetData(linkedHolopad.Comp.Hologram.Value.Owner, TypingIndicatorVisuals.IsTyping, false);
  451. linkedHolopad.Comp.Hologram.Value.Comp.LinkedEntity = user;
  452. Dirty(linkedHolopad.Comp.Hologram.Value);
  453. }
  454. }
  455. private void ShutDownHolopad(Entity<HolopadComponent> entity)
  456. {
  457. entity.Comp.ControlLockoutOwner = null;
  458. if (entity.Comp.Hologram != null)
  459. DeleteHologram(entity.Comp.Hologram.Value, entity);
  460. if (entity.Comp.User != null)
  461. {
  462. // Check if the associated holopad user is an AI
  463. if (TryComp<StationAiHeldComponent>(entity.Comp.User, out var stationAiHeld) &&
  464. _stationAiSystem.TryGetCore(entity.Comp.User.Value, out var stationAiCore))
  465. {
  466. // Return the AI eye to free roaming
  467. _stationAiSystem.SwitchRemoteEntityMode(stationAiCore, true);
  468. // If the AI core is still broadcasting, end its calls
  469. if (entity.Owner != stationAiCore.Owner &&
  470. TryComp<TelephoneComponent>(stationAiCore, out var stationAiCoreTelephone) &&
  471. _telephoneSystem.IsTelephoneEngaged((stationAiCore.Owner, stationAiCoreTelephone)))
  472. {
  473. _telephoneSystem.EndTelephoneCalls((stationAiCore.Owner, stationAiCoreTelephone));
  474. }
  475. }
  476. UnlinkHolopadFromUser(entity, entity.Comp.User.Value);
  477. }
  478. Dirty(entity);
  479. }
  480. private void ActivateProjector(Entity<HolopadComponent> entity, EntityUid user)
  481. {
  482. if (!TryComp<TelephoneComponent>(entity, out var receiverTelephone))
  483. return;
  484. var receiver = new Entity<TelephoneComponent>(entity, receiverTelephone);
  485. if (!TryComp<StationAiHeldComponent>(user, out var userAiHeld))
  486. return;
  487. if (!_stationAiSystem.TryGetCore(user, out var stationAiCore) ||
  488. stationAiCore.Comp?.RemoteEntity == null)
  489. return;
  490. if (!TryComp<TelephoneComponent>(stationAiCore, out var stationAiTelephone))
  491. return;
  492. if (!TryComp<HolopadComponent>(stationAiCore, out var stationAiHolopad))
  493. return;
  494. var source = new Entity<TelephoneComponent>(stationAiCore, stationAiTelephone);
  495. // Check if the AI is unable to activate the projector (unlikely this will ever pass; its just a safeguard)
  496. if (!_telephoneSystem.IsSourceInRangeOfReceiver(source, receiver))
  497. {
  498. _popupSystem.PopupEntity(Loc.GetString("holopad-ai-is-unable-to-activate-projector"), receiver, user);
  499. return;
  500. }
  501. // Terminate any calls that the core is hosting and immediately connect to the receiver
  502. _telephoneSystem.TerminateTelephoneCalls(source);
  503. var callOptions = new TelephoneCallOptions()
  504. {
  505. ForceConnect = true,
  506. MuteReceiver = true
  507. };
  508. _telephoneSystem.CallTelephone(source, receiver, user, callOptions);
  509. if (!_telephoneSystem.IsSourceConnectedToReceiver(source, receiver))
  510. return;
  511. LinkHolopadToUser((stationAiCore, stationAiHolopad), user);
  512. // Switch the AI's perspective from free roaming to the target holopad
  513. _xformSystem.SetCoordinates(stationAiCore.Comp.RemoteEntity.Value, Transform(entity).Coordinates);
  514. _stationAiSystem.SwitchRemoteEntityMode(stationAiCore, false);
  515. // Open the holopad UI if it hasn't been opened yet
  516. if (TryComp<UserInterfaceComponent>(entity, out var entityUserInterfaceComponent))
  517. _userInterfaceSystem.OpenUi((entity, entityUserInterfaceComponent), HolopadUiKey.InteractionWindow, user);
  518. }
  519. private void ExecuteBroadcast(Entity<HolopadComponent> source, EntityUid user)
  520. {
  521. if (!TryComp<TelephoneComponent>(source, out var sourceTelephone))
  522. return;
  523. var sourceTelephoneEntity = new Entity<TelephoneComponent>(source, sourceTelephone);
  524. _telephoneSystem.TerminateTelephoneCalls(sourceTelephoneEntity);
  525. // Find all holopads in range of the source
  526. var sourceXform = Transform(source);
  527. var receivers = new HashSet<Entity<TelephoneComponent>>();
  528. var query = AllEntityQuery<HolopadComponent, TelephoneComponent, TransformComponent>();
  529. while (query.MoveNext(out var receiver, out var receiverHolopad, out var receiverTelephone, out var receiverXform))
  530. {
  531. var receiverTelephoneEntity = new Entity<TelephoneComponent>(receiver, receiverTelephone);
  532. if (sourceTelephoneEntity == receiverTelephoneEntity ||
  533. !_telephoneSystem.IsSourceAbleToReachReceiver(sourceTelephoneEntity, receiverTelephoneEntity))
  534. continue;
  535. // If any holopads in range are on broadcast cooldown, exit
  536. if (IsHolopadBroadcastOnCoolDown((receiver, receiverHolopad)))
  537. return;
  538. receivers.Add(receiverTelephoneEntity);
  539. }
  540. var options = new TelephoneCallOptions()
  541. {
  542. ForceConnect = true,
  543. MuteReceiver = true,
  544. };
  545. _telephoneSystem.BroadcastCallToTelephones(sourceTelephoneEntity, receivers, user, options);
  546. if (!_telephoneSystem.IsTelephoneEngaged(sourceTelephoneEntity))
  547. return;
  548. // Link to the user after all the calls have been placed,
  549. // so we only need to sync all the holograms once
  550. LinkHolopadToUser(source, user);
  551. // Lock out the controls of all involved holopads for a set duration
  552. source.Comp.ControlLockoutOwner = user;
  553. source.Comp.ControlLockoutStartTime = _timing.CurTime;
  554. Dirty(source);
  555. foreach (var receiver in GetLinkedHolopads(source))
  556. {
  557. receiver.Comp.ControlLockoutOwner = user;
  558. receiver.Comp.ControlLockoutStartTime = _timing.CurTime;
  559. Dirty(receiver);
  560. }
  561. }
  562. private HashSet<Entity<HolopadComponent>> GetLinkedHolopads(Entity<HolopadComponent> entity)
  563. {
  564. var linkedHolopads = new HashSet<Entity<HolopadComponent>>();
  565. if (!TryComp<TelephoneComponent>(entity, out var holopadTelephone))
  566. return linkedHolopads;
  567. foreach (var linkedEnt in holopadTelephone.LinkedTelephones)
  568. {
  569. if (!TryComp<HolopadComponent>(linkedEnt, out var linkedHolopad))
  570. continue;
  571. linkedHolopads.Add((linkedEnt, linkedHolopad));
  572. }
  573. return linkedHolopads;
  574. }
  575. private void SetHolopadAmbientState(Entity<HolopadComponent> entity, bool isEnabled)
  576. {
  577. if (TryComp<PointLightComponent>(entity, out var pointLight))
  578. _pointLightSystem.SetEnabled(entity, isEnabled, pointLight);
  579. if (TryComp<AmbientSoundComponent>(entity, out var ambientSound))
  580. _ambientSoundSystem.SetAmbience(entity, isEnabled, ambientSound);
  581. }
  582. }