Files
tbd-station-14/Content.Server/Telephone/TelephoneSystem.cs
Pieter-Jan Briers 0c97520276 Fix usages of TryIndex() (#39124)
* Fix usages of TryIndex()

Most usages of TryIndex() were using it incorrectly. Checking whether prototype IDs specified in prototypes actually existed before using them. This is not appropriate as it's just hiding bugs that should be getting caught by the YAML linter and other tools. (#39115)

This then resulted in TryIndex() getting modified to log errors (94f98073b0), which is incorrect as it causes false-positive errors in proper uses of the API: external data validation. (#39098)

This commit goes through and checks every call site of TryIndex() to see whether they were correct. Most call sites were replaced with the new Resolve(), which is suitable for these "defensive programming" use cases.

Fixes #39115

Breaking change: while doing this I noticed IdCardComponent and related systems were erroneously using ProtoId<AccessLevelPrototype> for job prototypes. This has been corrected.

* fix tests

---------

Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com>
2025-09-09 18:17:56 +02:00

495 lines
18 KiB
C#

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<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 = 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.Resolve(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);
}
}