TelephoneSystem.cs 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495
  1. using Content.Server.Access.Systems;
  2. using Content.Server.Administration.Logs;
  3. using Content.Server.Chat.Systems;
  4. using Content.Server.Interaction;
  5. using Content.Server.Power.EntitySystems;
  6. using Content.Server.Speech;
  7. using Content.Server.Speech.Components;
  8. using Content.Shared.Chat;
  9. using Content.Shared.Database;
  10. using Content.Shared.Labels.Components;
  11. using Content.Shared.Mind.Components;
  12. using Content.Shared.Power;
  13. using Content.Shared.Silicons.StationAi;
  14. using Content.Shared.Silicons.Borgs.Components;
  15. using Content.Shared.Speech;
  16. using Content.Shared.Telephone;
  17. using Robust.Server.GameObjects;
  18. using Robust.Shared.Audio.Systems;
  19. using Robust.Shared.Timing;
  20. using Robust.Shared.Utility;
  21. using Robust.Shared.Prototypes;
  22. using Robust.Shared.Random;
  23. using Robust.Shared.Replays;
  24. using System.Linq;
  25. namespace Content.Server.Telephone;
  26. public sealed class TelephoneSystem : SharedTelephoneSystem
  27. {
  28. [Dependency] private readonly AppearanceSystem _appearanceSystem = default!;
  29. [Dependency] private readonly InteractionSystem _interaction = default!;
  30. [Dependency] private readonly IdCardSystem _idCardSystem = default!;
  31. [Dependency] private readonly SharedAudioSystem _audio = default!;
  32. [Dependency] private readonly ChatSystem _chat = default!;
  33. [Dependency] private readonly IPrototypeManager _prototype = default!;
  34. [Dependency] private readonly IGameTiming _timing = default!;
  35. [Dependency] private readonly IRobustRandom _random = default!;
  36. [Dependency] private readonly IAdminLogManager _adminLogger = default!;
  37. [Dependency] private readonly IReplayRecordingManager _replay = default!;
  38. // Has set used to prevent telephone feedback loops
  39. private HashSet<(EntityUid, string, Entity<TelephoneComponent>)> _recentChatMessages = new();
  40. public override void Initialize()
  41. {
  42. base.Initialize();
  43. SubscribeLocalEvent<TelephoneComponent, ComponentShutdown>(OnComponentShutdown);
  44. SubscribeLocalEvent<TelephoneComponent, PowerChangedEvent>(OnPowerChanged);
  45. SubscribeLocalEvent<TelephoneComponent, ListenAttemptEvent>(OnAttemptListen);
  46. SubscribeLocalEvent<TelephoneComponent, ListenEvent>(OnListen);
  47. SubscribeLocalEvent<TelephoneComponent, TelephoneMessageReceivedEvent>(OnTelephoneMessageReceived);
  48. }
  49. #region: Events
  50. private void OnComponentShutdown(Entity<TelephoneComponent> entity, ref ComponentShutdown ev)
  51. {
  52. TerminateTelephoneCalls(entity);
  53. }
  54. private void OnPowerChanged(Entity<TelephoneComponent> entity, ref PowerChangedEvent ev)
  55. {
  56. if (!ev.Powered)
  57. TerminateTelephoneCalls(entity);
  58. }
  59. private void OnAttemptListen(Entity<TelephoneComponent> entity, ref ListenAttemptEvent args)
  60. {
  61. if (!IsTelephonePowered(entity) ||
  62. !IsTelephoneEngaged(entity) ||
  63. entity.Comp.Muted ||
  64. !_interaction.InRangeUnobstructed(args.Source, entity.Owner, 0))
  65. {
  66. args.Cancel();
  67. }
  68. }
  69. private void OnListen(Entity<TelephoneComponent> entity, ref ListenEvent args)
  70. {
  71. if (args.Source == entity.Owner)
  72. return;
  73. // Ignore background chatter from non-player entities
  74. if (!HasComp<MindContainerComponent>(args.Source))
  75. return;
  76. // Simple check to make sure that we haven't sent this message already this frame
  77. if (!_recentChatMessages.Add((args.Source, args.Message, entity)))
  78. return;
  79. SendTelephoneMessage(args.Source, args.Message, entity);
  80. }
  81. private void OnTelephoneMessageReceived(Entity<TelephoneComponent> entity, ref TelephoneMessageReceivedEvent args)
  82. {
  83. // Prevent message feedback loops
  84. if (entity == args.TelephoneSource)
  85. return;
  86. if (!IsTelephonePowered(entity) ||
  87. !IsSourceConnectedToReceiver(args.TelephoneSource, entity))
  88. return;
  89. var nameEv = new TransformSpeakerNameEvent(args.MessageSource, Name(args.MessageSource));
  90. RaiseLocalEvent(args.MessageSource, nameEv);
  91. // Determine if speech should be relayed via the telephone itself or a designated speaker
  92. var speaker = entity.Comp.Speaker != null ? entity.Comp.Speaker.Value.Owner : entity.Owner;
  93. var name = Loc.GetString("chat-telephone-name-relay",
  94. ("originalName", nameEv.VoiceName),
  95. ("speaker", Name(speaker)));
  96. var range = args.TelephoneSource.Comp.LinkedTelephones.Count > 1 ? ChatTransmitRange.HideChat : ChatTransmitRange.GhostRangeLimit;
  97. var volume = entity.Comp.SpeakerVolume == TelephoneVolume.Speak ? InGameICChatType.Speak : InGameICChatType.Whisper;
  98. _chat.TrySendInGameICMessage(speaker, args.Message, volume, range, nameOverride: name, checkRadioPrefix: false);
  99. }
  100. #endregion
  101. public override void Update(float frameTime)
  102. {
  103. base.Update(frameTime);
  104. var query = EntityManager.EntityQueryEnumerator<TelephoneComponent>();
  105. while (query.MoveNext(out var uid, out var telephone))
  106. {
  107. var entity = new Entity<TelephoneComponent>(uid, telephone);
  108. if (IsTelephoneEngaged(entity))
  109. {
  110. foreach (var receiver in telephone.LinkedTelephones)
  111. {
  112. if (!IsSourceInRangeOfReceiver(entity, receiver) &&
  113. !IsSourceInRangeOfReceiver(receiver, entity))
  114. {
  115. EndTelephoneCall(entity, receiver);
  116. }
  117. }
  118. }
  119. switch (telephone.CurrentState)
  120. {
  121. // Try to play ring tone if ringing
  122. case TelephoneState.Ringing:
  123. if (_timing.CurTime > telephone.StateStartTime + TimeSpan.FromSeconds(telephone.RingingTimeout))
  124. EndTelephoneCalls(entity);
  125. else if (telephone.RingTone != null &&
  126. _timing.CurTime > telephone.NextRingToneTime)
  127. {
  128. _audio.PlayPvs(telephone.RingTone, uid);
  129. telephone.NextRingToneTime = _timing.CurTime + TimeSpan.FromSeconds(telephone.RingInterval);
  130. }
  131. break;
  132. // Try to hang up if there has been no recent in-call activity
  133. case TelephoneState.InCall:
  134. if (_timing.CurTime > telephone.StateStartTime + TimeSpan.FromSeconds(telephone.IdlingTimeout))
  135. EndTelephoneCalls(entity);
  136. break;
  137. // Try to terminate if the telephone has finished hanging up
  138. case TelephoneState.EndingCall:
  139. if (_timing.CurTime > telephone.StateStartTime + TimeSpan.FromSeconds(telephone.HangingUpTimeout))
  140. TerminateTelephoneCalls(entity);
  141. break;
  142. }
  143. }
  144. _recentChatMessages.Clear();
  145. }
  146. public void BroadcastCallToTelephones(Entity<TelephoneComponent> source, HashSet<Entity<TelephoneComponent>> receivers, EntityUid user, TelephoneCallOptions? options = null)
  147. {
  148. if (IsTelephoneEngaged(source))
  149. return;
  150. foreach (var receiver in receivers)
  151. TryCallTelephone(source, receiver, user, options);
  152. // If no connections could be made, hang up the telephone
  153. if (!IsTelephoneEngaged(source))
  154. EndTelephoneCalls(source);
  155. }
  156. public void CallTelephone(Entity<TelephoneComponent> source, Entity<TelephoneComponent> receiver, EntityUid user, TelephoneCallOptions? options = null)
  157. {
  158. if (IsTelephoneEngaged(source))
  159. return;
  160. if (!TryCallTelephone(source, receiver, user, options))
  161. EndTelephoneCalls(source);
  162. }
  163. private bool TryCallTelephone(Entity<TelephoneComponent> source, Entity<TelephoneComponent> receiver, EntityUid user, TelephoneCallOptions? options = null)
  164. {
  165. if (!IsSourceAbleToReachReceiver(source, receiver) && options?.IgnoreRange != true)
  166. return false;
  167. if (IsTelephoneEngaged(receiver) &&
  168. options?.ForceConnect != true &&
  169. options?.ForceJoin != true)
  170. return false;
  171. var evCallAttempt = new TelephoneCallAttemptEvent(source, receiver, user);
  172. RaiseLocalEvent(source, ref evCallAttempt);
  173. if (evCallAttempt.Cancelled)
  174. return false;
  175. if (options?.ForceConnect == true)
  176. TerminateTelephoneCalls(receiver);
  177. source.Comp.LinkedTelephones.Add(receiver);
  178. source.Comp.Muted = options?.MuteSource == true;
  179. var callerInfo = GetNameAndJobOfCallingEntity(user);
  180. // Base the name of the device on its label
  181. string? deviceName = null;
  182. if (TryComp<LabelComponent>(source, out var label))
  183. deviceName = label.CurrentLabel;
  184. receiver.Comp.LastCallerId = (callerInfo.Item1, callerInfo.Item2, deviceName); // This will be networked when the state changes
  185. receiver.Comp.LinkedTelephones.Add(source);
  186. receiver.Comp.Muted = options?.MuteReceiver == true;
  187. // Try to open a line of communication immediately
  188. if (options?.ForceConnect == true ||
  189. (options?.ForceJoin == true && receiver.Comp.CurrentState == TelephoneState.InCall))
  190. {
  191. CommenceTelephoneCall(source, receiver);
  192. return true;
  193. }
  194. // Otherwise start ringing the receiver
  195. SetTelephoneState(source, TelephoneState.Calling);
  196. SetTelephoneState(receiver, TelephoneState.Ringing);
  197. return true;
  198. }
  199. public void AnswerTelephone(Entity<TelephoneComponent> receiver, EntityUid user)
  200. {
  201. if (receiver.Comp.CurrentState != TelephoneState.Ringing)
  202. return;
  203. // If the telephone isn't linked, or is linked to more than one telephone,
  204. // you shouldn't need to answer the call. If you do need to answer it,
  205. // you'll need to be handled this a different way
  206. if (receiver.Comp.LinkedTelephones.Count != 1)
  207. return;
  208. var source = receiver.Comp.LinkedTelephones.First();
  209. CommenceTelephoneCall(source, receiver);
  210. }
  211. private void CommenceTelephoneCall(Entity<TelephoneComponent> source, Entity<TelephoneComponent> receiver)
  212. {
  213. SetTelephoneState(source, TelephoneState.InCall);
  214. SetTelephoneState(receiver, TelephoneState.InCall);
  215. SetTelephoneMicrophoneState(source, true);
  216. SetTelephoneMicrophoneState(receiver, true);
  217. var evSource = new TelephoneCallCommencedEvent(receiver);
  218. var evReceiver = new TelephoneCallCommencedEvent(source);
  219. RaiseLocalEvent(source, ref evSource);
  220. RaiseLocalEvent(receiver, ref evReceiver);
  221. }
  222. public void EndTelephoneCall(Entity<TelephoneComponent> source, Entity<TelephoneComponent> receiver)
  223. {
  224. source.Comp.LinkedTelephones.Remove(receiver);
  225. receiver.Comp.LinkedTelephones.Remove(source);
  226. if (!IsTelephoneEngaged(source))
  227. EndTelephoneCalls(source);
  228. if (!IsTelephoneEngaged(receiver))
  229. EndTelephoneCalls(receiver);
  230. }
  231. public void EndTelephoneCalls(Entity<TelephoneComponent> entity)
  232. {
  233. // No need to end any calls if the telephone is already ending a call
  234. if (entity.Comp.CurrentState == TelephoneState.EndingCall)
  235. return;
  236. HandleEndingTelephoneCalls(entity, TelephoneState.EndingCall);
  237. var ev = new TelephoneCallEndedEvent();
  238. RaiseLocalEvent(entity, ref ev);
  239. }
  240. public void TerminateTelephoneCalls(Entity<TelephoneComponent> entity)
  241. {
  242. // No need to terminate any calls if the telephone is idle
  243. if (entity.Comp.CurrentState == TelephoneState.Idle)
  244. return;
  245. HandleEndingTelephoneCalls(entity, TelephoneState.Idle);
  246. }
  247. private void HandleEndingTelephoneCalls(Entity<TelephoneComponent> entity, TelephoneState newState)
  248. {
  249. foreach (var linkedTelephone in entity.Comp.LinkedTelephones)
  250. {
  251. if (!linkedTelephone.Comp.LinkedTelephones.Remove(entity))
  252. continue;
  253. if (!IsTelephoneEngaged(linkedTelephone))
  254. EndTelephoneCalls(linkedTelephone);
  255. }
  256. entity.Comp.LinkedTelephones.Clear();
  257. entity.Comp.Muted = false;
  258. SetTelephoneState(entity, newState);
  259. SetTelephoneMicrophoneState(entity, false);
  260. }
  261. private void SendTelephoneMessage(EntityUid messageSource, string message, Entity<TelephoneComponent> source, bool escapeMarkup = true)
  262. {
  263. // This method assumes that you've already checked that this
  264. // telephone is able to transmit messages and that it can
  265. // send messages to any telephones linked to it
  266. var ev = new TransformSpeakerNameEvent(messageSource, MetaData(messageSource).EntityName);
  267. RaiseLocalEvent(messageSource, ev);
  268. var name = ev.VoiceName;
  269. name = FormattedMessage.EscapeText(name);
  270. SpeechVerbPrototype speech;
  271. if (ev.SpeechVerb != null && _prototype.TryIndex(ev.SpeechVerb, out var evntProto))
  272. speech = evntProto;
  273. else
  274. speech = _chat.GetSpeechVerb(messageSource, message);
  275. var content = escapeMarkup
  276. ? FormattedMessage.EscapeText(message)
  277. : message;
  278. var wrappedMessage = Loc.GetString(speech.Bold ? "chat-telephone-message-wrap-bold" : "chat-telephone-message-wrap",
  279. ("color", Color.White),
  280. ("fontType", speech.FontId),
  281. ("fontSize", speech.FontSize),
  282. ("verb", Loc.GetString(_random.Pick(speech.SpeechVerbStrings))),
  283. ("name", name),
  284. ("message", content));
  285. var chat = new ChatMessage(
  286. ChatChannel.Local,
  287. message,
  288. wrappedMessage,
  289. NetEntity.Invalid,
  290. null);
  291. var chatMsg = new MsgChatMessage { Message = chat };
  292. var evSentMessage = new TelephoneMessageSentEvent(message, chatMsg, messageSource);
  293. RaiseLocalEvent(source, ref evSentMessage);
  294. source.Comp.StateStartTime = _timing.CurTime;
  295. var evReceivedMessage = new TelephoneMessageReceivedEvent(message, chatMsg, messageSource, source);
  296. foreach (var receiver in source.Comp.LinkedTelephones)
  297. {
  298. RaiseLocalEvent(receiver, ref evReceivedMessage);
  299. receiver.Comp.StateStartTime = _timing.CurTime;
  300. }
  301. if (name != Name(messageSource))
  302. _adminLogger.Add(LogType.Chat, LogImpact.Low, $"Telephone message from {ToPrettyString(messageSource):user} as {name} on {source}: {message}");
  303. else
  304. _adminLogger.Add(LogType.Chat, LogImpact.Low, $"Telephone message from {ToPrettyString(messageSource):user} on {source}: {message}");
  305. _replay.RecordServerMessage(chat);
  306. }
  307. private void SetTelephoneState(Entity<TelephoneComponent> entity, TelephoneState newState)
  308. {
  309. var oldState = entity.Comp.CurrentState;
  310. entity.Comp.CurrentState = newState;
  311. entity.Comp.StateStartTime = _timing.CurTime;
  312. Dirty(entity);
  313. _appearanceSystem.SetData(entity, TelephoneVisuals.Key, entity.Comp.CurrentState);
  314. var ev = new TelephoneStateChangeEvent(oldState, newState);
  315. RaiseLocalEvent(entity, ref ev);
  316. }
  317. private void SetTelephoneMicrophoneState(Entity<TelephoneComponent> entity, bool microphoneOn)
  318. {
  319. if (microphoneOn && !HasComp<ActiveListenerComponent>(entity))
  320. {
  321. var activeListener = AddComp<ActiveListenerComponent>(entity);
  322. activeListener.Range = entity.Comp.ListeningRange;
  323. }
  324. if (!microphoneOn && HasComp<ActiveListenerComponent>(entity))
  325. {
  326. RemComp<ActiveListenerComponent>(entity);
  327. }
  328. }
  329. public void SetSpeakerForTelephone(Entity<TelephoneComponent> entity, Entity<SpeechComponent>? speaker)
  330. {
  331. entity.Comp.Speaker = speaker;
  332. }
  333. private (string?, string?) GetNameAndJobOfCallingEntity(EntityUid uid)
  334. {
  335. string? presumedName = null;
  336. string? presumedJob = null;
  337. if (HasComp<StationAiHeldComponent>(uid) || HasComp<BorgChassisComponent>(uid))
  338. {
  339. presumedName = Name(uid);
  340. return (presumedName, presumedJob);
  341. }
  342. if (_idCardSystem.TryFindIdCard(uid, out var idCard))
  343. {
  344. presumedName = string.IsNullOrWhiteSpace(idCard.Comp.FullName) ? null : idCard.Comp.FullName;
  345. presumedJob = idCard.Comp.LocalizedJobTitle;
  346. }
  347. return (presumedName, presumedJob);
  348. }
  349. public bool IsSourceAbleToReachReceiver(Entity<TelephoneComponent> source, Entity<TelephoneComponent> receiver)
  350. {
  351. if (source == receiver ||
  352. !IsTelephonePowered(source) ||
  353. !IsTelephonePowered(receiver) ||
  354. !IsSourceInRangeOfReceiver(source, receiver))
  355. {
  356. return false;
  357. }
  358. return true;
  359. }
  360. public bool IsSourceInRangeOfReceiver(Entity<TelephoneComponent> source, Entity<TelephoneComponent> receiver)
  361. {
  362. // Check if the source and receiver have compatible transmision / reception bandwidths
  363. if (!source.Comp.CompatibleRanges.Contains(receiver.Comp.TransmissionRange))
  364. return false;
  365. var sourceXform = Transform(source);
  366. var receiverXform = Transform(receiver);
  367. // Check if we should ignore a device thats on the same grid
  368. if (source.Comp.IgnoreTelephonesOnSameGrid &&
  369. source.Comp.TransmissionRange != TelephoneRange.Grid &&
  370. receiverXform.GridUid == sourceXform.GridUid)
  371. return false;
  372. switch (source.Comp.TransmissionRange)
  373. {
  374. case TelephoneRange.Grid:
  375. return sourceXform.GridUid == receiverXform.GridUid;
  376. case TelephoneRange.Map:
  377. return sourceXform.MapID == receiverXform.MapID;
  378. case TelephoneRange.Unlimited:
  379. return true;
  380. }
  381. return false;
  382. }
  383. public bool IsSourceConnectedToReceiver(Entity<TelephoneComponent> source, Entity<TelephoneComponent> receiver)
  384. {
  385. return source.Comp.LinkedTelephones.Contains(receiver);
  386. }
  387. public bool IsTelephonePowered(Entity<TelephoneComponent> entity)
  388. {
  389. return this.IsPowered(entity, EntityManager) || !entity.Comp.RequiresPower;
  390. }
  391. }