* 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>
495 lines
18 KiB
C#
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);
|
|
}
|
|
}
|