| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495 |
- 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.Server.Speech;
- using Content.Server.Speech.Components;
- 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.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<TelephoneComponent>)> _recentChatMessages = new();
- public override void Initialize()
- {
- base.Initialize();
- SubscribeLocalEvent<TelephoneComponent, ComponentShutdown>(OnComponentShutdown);
- SubscribeLocalEvent<TelephoneComponent, PowerChangedEvent>(OnPowerChanged);
- SubscribeLocalEvent<TelephoneComponent, ListenAttemptEvent>(OnAttemptListen);
- SubscribeLocalEvent<TelephoneComponent, ListenEvent>(OnListen);
- SubscribeLocalEvent<TelephoneComponent, TelephoneMessageReceivedEvent>(OnTelephoneMessageReceived);
- }
- #region: Events
- private void OnComponentShutdown(Entity<TelephoneComponent> entity, ref ComponentShutdown ev)
- {
- TerminateTelephoneCalls(entity);
- }
- private void OnPowerChanged(Entity<TelephoneComponent> entity, ref PowerChangedEvent ev)
- {
- if (!ev.Powered)
- TerminateTelephoneCalls(entity);
- }
- private void OnAttemptListen(Entity<TelephoneComponent> 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<TelephoneComponent> entity, ref ListenEvent args)
- {
- if (args.Source == entity.Owner)
- return;
- // Ignore background chatter from non-player entities
- if (!HasComp<MindContainerComponent>(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<TelephoneComponent> 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 = EntityManager.EntityQueryEnumerator<TelephoneComponent>();
- while (query.MoveNext(out var uid, out var telephone))
- {
- var entity = new Entity<TelephoneComponent>(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<TelephoneComponent> source, HashSet<Entity<TelephoneComponent>> 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<TelephoneComponent> source, Entity<TelephoneComponent> receiver, EntityUid user, TelephoneCallOptions? options = null)
- {
- if (IsTelephoneEngaged(source))
- return;
- if (!TryCallTelephone(source, receiver, user, options))
- EndTelephoneCalls(source);
- }
- private bool TryCallTelephone(Entity<TelephoneComponent> source, Entity<TelephoneComponent> 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<LabelComponent>(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<TelephoneComponent> 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<TelephoneComponent> source, Entity<TelephoneComponent> 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<TelephoneComponent> source, Entity<TelephoneComponent> 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<TelephoneComponent> 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<TelephoneComponent> 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<TelephoneComponent> 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<TelephoneComponent> 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<TelephoneComponent> 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<TelephoneComponent> entity, bool microphoneOn)
- {
- if (microphoneOn && !HasComp<ActiveListenerComponent>(entity))
- {
- var activeListener = AddComp<ActiveListenerComponent>(entity);
- activeListener.Range = entity.Comp.ListeningRange;
- }
- if (!microphoneOn && HasComp<ActiveListenerComponent>(entity))
- {
- RemComp<ActiveListenerComponent>(entity);
- }
- }
- public void SetSpeakerForTelephone(Entity<TelephoneComponent> entity, Entity<SpeechComponent>? speaker)
- {
- entity.Comp.Speaker = speaker;
- }
- private (string?, string?) GetNameAndJobOfCallingEntity(EntityUid uid)
- {
- string? presumedName = null;
- string? presumedJob = null;
- if (HasComp<StationAiHeldComponent>(uid) || HasComp<BorgChassisComponent>(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<TelephoneComponent> source, Entity<TelephoneComponent> receiver)
- {
- if (source == receiver ||
- !IsTelephonePowered(source) ||
- !IsTelephonePowered(receiver) ||
- !IsSourceInRangeOfReceiver(source, receiver))
- {
- return false;
- }
- return true;
- }
- public bool IsSourceInRangeOfReceiver(Entity<TelephoneComponent> source, Entity<TelephoneComponent> 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<TelephoneComponent> source, Entity<TelephoneComponent> receiver)
- {
- return source.Comp.LinkedTelephones.Contains(receiver);
- }
- public bool IsTelephonePowered(Entity<TelephoneComponent> entity)
- {
- return this.IsPowered(entity, EntityManager) || !entity.Comp.RequiresPower;
- }
- }
|