mirror of
https://github.com/WWhiteDreamProject/wwdpublic.git
synced 2026-04-17 21:48:58 +03:00
# Description
This PR fixes a bug whereby the Holopads weren't respecting languages,
and actually basically every machine in the entire game wasn't. There's
an in-general broader issue that I would reaaaaaally like if UI elements
in general could be differentiated by language, but that's a lot harder
to do than this fix. This was shockingly easy to fix actually.
# Changelog
🆑
- fix: Holopads now correctly respect the speaker's language, and
transmit said language to the receiver. They are no longer Universal
translators. Have fun yelling at people over the holopad in whatever
obscure language your character knows.
- fix: Nearly every machine that can speak, now correctly respects that
languages exist. For example, mice can now no longer understand what
vending machines are saying.
490 lines
18 KiB
C#
490 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.Server.Speech;
|
|
using Content.Server.Speech.Components;
|
|
using Content.Shared.Chat;
|
|
using Content.Shared.Database;
|
|
using Content.Shared.Mind.Components;
|
|
using Content.Shared.Power;
|
|
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;
|
|
using Content.Shared.Silicons.StationAi;
|
|
using Content.Shared.Silicons.Borgs.Components;
|
|
using Content.Server.Language;
|
|
|
|
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!;
|
|
[Dependency] private readonly LanguageSystem _language = 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, languageOverride: args.Language);
|
|
}
|
|
|
|
#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))
|
|
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);
|
|
receiver.Comp.LastCallerId = (callerInfo.Item1, callerInfo.Item2, Name(source)); // 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, _language.GetLanguage(messageSource));
|
|
|
|
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;
|
|
}
|
|
}
|