using Content.Server.Access.Systems; using Content.Server.Administration.Logs; using Content.Server.Chat.Systems; using Content.Server.Interaction; using Content.Server.Power.EntitySystems; using Content.Shared.Chat; using Content.Shared.Database; using Content.Shared.Labels.Components; using Content.Shared.Mind.Components; using Content.Shared.Power; using Content.Shared.Silicons.StationAi; using Content.Shared.Silicons.Borgs.Components; using Content.Shared.Speech; using Content.Shared.Speech.Components; using Content.Shared.Telephone; using Robust.Server.GameObjects; using Robust.Shared.Audio.Systems; using Robust.Shared.Timing; using Robust.Shared.Utility; using Robust.Shared.Prototypes; using Robust.Shared.Random; using Robust.Shared.Replays; using System.Linq; namespace Content.Server.Telephone; public sealed class TelephoneSystem : SharedTelephoneSystem { [Dependency] private readonly AppearanceSystem _appearanceSystem = default!; [Dependency] private readonly InteractionSystem _interaction = default!; [Dependency] private readonly IdCardSystem _idCardSystem = default!; [Dependency] private readonly SharedAudioSystem _audio = default!; [Dependency] private readonly ChatSystem _chat = default!; [Dependency] private readonly IPrototypeManager _prototype = default!; [Dependency] private readonly IGameTiming _timing = default!; [Dependency] private readonly IRobustRandom _random = default!; [Dependency] private readonly IAdminLogManager _adminLogger = default!; [Dependency] private readonly IReplayRecordingManager _replay = default!; // Has set used to prevent telephone feedback loops private HashSet<(EntityUid, string, Entity)> _recentChatMessages = new(); public override void Initialize() { base.Initialize(); SubscribeLocalEvent(OnComponentShutdown); SubscribeLocalEvent(OnPowerChanged); SubscribeLocalEvent(OnAttemptListen); SubscribeLocalEvent(OnListen); SubscribeLocalEvent(OnTelephoneMessageReceived); } #region: Events private void OnComponentShutdown(Entity entity, ref ComponentShutdown ev) { TerminateTelephoneCalls(entity); } private void OnPowerChanged(Entity entity, ref PowerChangedEvent ev) { if (!ev.Powered) TerminateTelephoneCalls(entity); } private void OnAttemptListen(Entity entity, ref ListenAttemptEvent args) { if (!IsTelephonePowered(entity) || !IsTelephoneEngaged(entity) || entity.Comp.Muted || !_interaction.InRangeUnobstructed(args.Source, entity.Owner, 0)) { args.Cancel(); } } private void OnListen(Entity entity, ref ListenEvent args) { if (args.Source == entity.Owner) return; // Ignore background chatter from non-player entities if (!HasComp(args.Source)) return; // Simple check to make sure that we haven't sent this message already this frame if (!_recentChatMessages.Add((args.Source, args.Message, entity))) return; SendTelephoneMessage(args.Source, args.Message, entity); } private void OnTelephoneMessageReceived(Entity entity, ref TelephoneMessageReceivedEvent args) { // Prevent message feedback loops if (entity == args.TelephoneSource) return; if (!IsTelephonePowered(entity) || !IsSourceConnectedToReceiver(args.TelephoneSource, entity)) return; var nameEv = new TransformSpeakerNameEvent(args.MessageSource, Name(args.MessageSource)); RaiseLocalEvent(args.MessageSource, nameEv); // Determine if speech should be relayed via the telephone itself or a designated speaker var speaker = entity.Comp.Speaker != null ? entity.Comp.Speaker.Value.Owner : entity.Owner; var name = Loc.GetString("chat-telephone-name-relay", ("originalName", nameEv.VoiceName), ("speaker", Name(speaker))); var range = args.TelephoneSource.Comp.LinkedTelephones.Count > 1 ? ChatTransmitRange.HideChat : ChatTransmitRange.GhostRangeLimit; var volume = entity.Comp.SpeakerVolume == TelephoneVolume.Speak ? InGameICChatType.Speak : InGameICChatType.Whisper; _chat.TrySendInGameICMessage(speaker, args.Message, volume, range, nameOverride: name, checkRadioPrefix: false); } #endregion public override void Update(float frameTime) { base.Update(frameTime); var query = EntityQueryEnumerator(); while (query.MoveNext(out var uid, out var telephone)) { var entity = new Entity(uid, telephone); if (IsTelephoneEngaged(entity)) { foreach (var receiver in telephone.LinkedTelephones) { if (!IsSourceInRangeOfReceiver(entity, receiver) && !IsSourceInRangeOfReceiver(receiver, entity)) { EndTelephoneCall(entity, receiver); } } } switch (telephone.CurrentState) { // Try to play ring tone if ringing case TelephoneState.Ringing: if (_timing.CurTime > telephone.StateStartTime + TimeSpan.FromSeconds(telephone.RingingTimeout)) EndTelephoneCalls(entity); else if (telephone.RingTone != null && _timing.CurTime > telephone.NextRingToneTime) { _audio.PlayPvs(telephone.RingTone, uid); telephone.NextRingToneTime = _timing.CurTime + TimeSpan.FromSeconds(telephone.RingInterval); } break; // Try to hang up if there has been no recent in-call activity case TelephoneState.InCall: if (_timing.CurTime > telephone.StateStartTime + TimeSpan.FromSeconds(telephone.IdlingTimeout)) EndTelephoneCalls(entity); break; // Try to terminate if the telephone has finished hanging up case TelephoneState.EndingCall: if (_timing.CurTime > telephone.StateStartTime + TimeSpan.FromSeconds(telephone.HangingUpTimeout)) TerminateTelephoneCalls(entity); break; } } _recentChatMessages.Clear(); } public void BroadcastCallToTelephones(Entity source, HashSet> receivers, EntityUid user, TelephoneCallOptions? options = null) { if (IsTelephoneEngaged(source)) return; foreach (var receiver in receivers) TryCallTelephone(source, receiver, user, options); // If no connections could be made, hang up the telephone if (!IsTelephoneEngaged(source)) EndTelephoneCalls(source); } public void CallTelephone(Entity source, Entity receiver, EntityUid user, TelephoneCallOptions? options = null) { if (IsTelephoneEngaged(source)) return; if (!TryCallTelephone(source, receiver, user, options)) EndTelephoneCalls(source); } private bool TryCallTelephone(Entity source, Entity receiver, EntityUid user, TelephoneCallOptions? options = null) { if (!IsSourceAbleToReachReceiver(source, receiver) && options?.IgnoreRange != true) return false; if (IsTelephoneEngaged(receiver) && options?.ForceConnect != true && options?.ForceJoin != true) return false; var evCallAttempt = new TelephoneCallAttemptEvent(source, receiver, user); RaiseLocalEvent(source, ref evCallAttempt); if (evCallAttempt.Cancelled) return false; if (options?.ForceConnect == true) TerminateTelephoneCalls(receiver); source.Comp.LinkedTelephones.Add(receiver); source.Comp.Muted = options?.MuteSource == true; var callerInfo = GetNameAndJobOfCallingEntity(user); // Base the name of the device on its label string? deviceName = null; if (TryComp(source, out var label)) deviceName = label.CurrentLabel; receiver.Comp.LastCallerId = (callerInfo.Item1, callerInfo.Item2, deviceName); // This will be networked when the state changes receiver.Comp.LinkedTelephones.Add(source); receiver.Comp.Muted = options?.MuteReceiver == true; // Try to open a line of communication immediately if (options?.ForceConnect == true || (options?.ForceJoin == true && receiver.Comp.CurrentState == TelephoneState.InCall)) { CommenceTelephoneCall(source, receiver); return true; } // Otherwise start ringing the receiver SetTelephoneState(source, TelephoneState.Calling); SetTelephoneState(receiver, TelephoneState.Ringing); return true; } public void AnswerTelephone(Entity receiver, EntityUid user) { if (receiver.Comp.CurrentState != TelephoneState.Ringing) return; // If the telephone isn't linked, or is linked to more than one telephone, // you shouldn't need to answer the call. If you do need to answer it, // you'll need to be handled this a different way if (receiver.Comp.LinkedTelephones.Count != 1) return; var source = receiver.Comp.LinkedTelephones.First(); CommenceTelephoneCall(source, receiver); } private void CommenceTelephoneCall(Entity source, Entity receiver) { SetTelephoneState(source, TelephoneState.InCall); SetTelephoneState(receiver, TelephoneState.InCall); SetTelephoneMicrophoneState(source, true); SetTelephoneMicrophoneState(receiver, true); var evSource = new TelephoneCallCommencedEvent(receiver); var evReceiver = new TelephoneCallCommencedEvent(source); RaiseLocalEvent(source, ref evSource); RaiseLocalEvent(receiver, ref evReceiver); } public void EndTelephoneCall(Entity source, Entity receiver) { source.Comp.LinkedTelephones.Remove(receiver); receiver.Comp.LinkedTelephones.Remove(source); if (!IsTelephoneEngaged(source)) EndTelephoneCalls(source); if (!IsTelephoneEngaged(receiver)) EndTelephoneCalls(receiver); } public void EndTelephoneCalls(Entity entity) { // No need to end any calls if the telephone is already ending a call if (entity.Comp.CurrentState == TelephoneState.EndingCall) return; HandleEndingTelephoneCalls(entity, TelephoneState.EndingCall); var ev = new TelephoneCallEndedEvent(); RaiseLocalEvent(entity, ref ev); } public void TerminateTelephoneCalls(Entity entity) { // No need to terminate any calls if the telephone is idle if (entity.Comp.CurrentState == TelephoneState.Idle) return; HandleEndingTelephoneCalls(entity, TelephoneState.Idle); } private void HandleEndingTelephoneCalls(Entity entity, TelephoneState newState) { foreach (var linkedTelephone in entity.Comp.LinkedTelephones) { if (!linkedTelephone.Comp.LinkedTelephones.Remove(entity)) continue; if (!IsTelephoneEngaged(linkedTelephone)) EndTelephoneCalls(linkedTelephone); } entity.Comp.LinkedTelephones.Clear(); entity.Comp.Muted = false; SetTelephoneState(entity, newState); SetTelephoneMicrophoneState(entity, false); } private void SendTelephoneMessage(EntityUid messageSource, string message, Entity source, bool escapeMarkup = true) { // This method assumes that you've already checked that this // telephone is able to transmit messages and that it can // send messages to any telephones linked to it var ev = new TransformSpeakerNameEvent(messageSource, MetaData(messageSource).EntityName); RaiseLocalEvent(messageSource, ev); var name = ev.VoiceName; name = FormattedMessage.EscapeText(name); SpeechVerbPrototype speech; if (ev.SpeechVerb != null && _prototype.TryIndex(ev.SpeechVerb, out var evntProto)) speech = evntProto; else speech = _chat.GetSpeechVerb(messageSource, message); var content = escapeMarkup ? FormattedMessage.EscapeText(message) : message; var wrappedMessage = Loc.GetString(speech.Bold ? "chat-telephone-message-wrap-bold" : "chat-telephone-message-wrap", ("color", Color.White), ("fontType", speech.FontId), ("fontSize", speech.FontSize), ("verb", Loc.GetString(_random.Pick(speech.SpeechVerbStrings))), ("name", name), ("message", content)); var chat = new ChatMessage( ChatChannel.Local, message, wrappedMessage, NetEntity.Invalid, null); var chatMsg = new MsgChatMessage { Message = chat }; var evSentMessage = new TelephoneMessageSentEvent(message, chatMsg, messageSource); RaiseLocalEvent(source, ref evSentMessage); source.Comp.StateStartTime = _timing.CurTime; var evReceivedMessage = new TelephoneMessageReceivedEvent(message, chatMsg, messageSource, source); foreach (var receiver in source.Comp.LinkedTelephones) { RaiseLocalEvent(receiver, ref evReceivedMessage); receiver.Comp.StateStartTime = _timing.CurTime; } if (name != Name(messageSource)) _adminLogger.Add(LogType.Chat, LogImpact.Low, $"Telephone message from {ToPrettyString(messageSource):user} as {name} on {source}: {message}"); else _adminLogger.Add(LogType.Chat, LogImpact.Low, $"Telephone message from {ToPrettyString(messageSource):user} on {source}: {message}"); _replay.RecordServerMessage(chat); } private void SetTelephoneState(Entity entity, TelephoneState newState) { var oldState = entity.Comp.CurrentState; entity.Comp.CurrentState = newState; entity.Comp.StateStartTime = _timing.CurTime; Dirty(entity); _appearanceSystem.SetData(entity, TelephoneVisuals.Key, entity.Comp.CurrentState); var ev = new TelephoneStateChangeEvent(oldState, newState); RaiseLocalEvent(entity, ref ev); } private void SetTelephoneMicrophoneState(Entity entity, bool microphoneOn) { if (microphoneOn && !HasComp(entity)) { var activeListener = AddComp(entity); activeListener.Range = entity.Comp.ListeningRange; } if (!microphoneOn && HasComp(entity)) { RemComp(entity); } } public void SetSpeakerForTelephone(Entity entity, Entity? speaker) { entity.Comp.Speaker = speaker; } private (string?, string?) GetNameAndJobOfCallingEntity(EntityUid uid) { string? presumedName = null; string? presumedJob = null; if (HasComp(uid) || HasComp(uid)) { presumedName = Name(uid); return (presumedName, presumedJob); } if (_idCardSystem.TryFindIdCard(uid, out var idCard)) { presumedName = string.IsNullOrWhiteSpace(idCard.Comp.FullName) ? null : idCard.Comp.FullName; presumedJob = idCard.Comp.LocalizedJobTitle; } return (presumedName, presumedJob); } public bool IsSourceAbleToReachReceiver(Entity source, Entity receiver) { if (source == receiver || !IsTelephonePowered(source) || !IsTelephonePowered(receiver) || !IsSourceInRangeOfReceiver(source, receiver)) { return false; } return true; } public bool IsSourceInRangeOfReceiver(Entity source, Entity receiver) { // Check if the source and receiver have compatible transmision / reception bandwidths if (!source.Comp.CompatibleRanges.Contains(receiver.Comp.TransmissionRange)) return false; var sourceXform = Transform(source); var receiverXform = Transform(receiver); // Check if we should ignore a device thats on the same grid if (source.Comp.IgnoreTelephonesOnSameGrid && source.Comp.TransmissionRange != TelephoneRange.Grid && receiverXform.GridUid == sourceXform.GridUid) return false; switch (source.Comp.TransmissionRange) { case TelephoneRange.Grid: return sourceXform.GridUid == receiverXform.GridUid; case TelephoneRange.Map: return sourceXform.MapID == receiverXform.MapID; case TelephoneRange.Unlimited: return true; } return false; } public bool IsSourceConnectedToReceiver(Entity source, Entity receiver) { return source.Comp.LinkedTelephones.Contains(receiver); } public bool IsTelephonePowered(Entity entity) { return this.IsPowered(entity, EntityManager); } }