Holopads (#32711)
* Initial resources commit * Initial code commit * Added additional resources * Continuing to build holopad and telephone systems * Added hologram shader * Added hologram system and entity * Holo calls now have a hologram of the user appear on them * Initial implementation of holopads transmitting nearby chatter * Added support for linking across multiple telephones/holopads/entities * Fixed a bunch of bugs * Tried simplifying holopad entity dependence, added support for mid-call user switching * Replaced PVS expansion with manually networked sprite states * Adjusted volume of ring tone * Added machine board * Minor features and tweaks * Resolving merge conflict * Recommit audio attributions * Telephone chat adjustments * Added support for AI interactions with holopads * Building the holopad UI * Holopad UI finished * Further UI tweaks * Station AI can hear local chatter when being projected from a holopad * Minor bug fixes * Added wire panels to holopads * Basic broadcasting * Start of emergency broadcasting code * Fixing issues with broadcasting * More work on emergency broadcasting * Updated holopad visuals * Added cooldown text to emergency broadcast and control lock out screen * Code clean up * Fixed issue with timing * Broadcasting now requires command access * Fixed some bugs * Added multiple holopad prototypes with different ranges * The AI no longer requires power to interact with holopads * Fixed some additional issues * Addressing more issues * Added emote support for holograms * Changed the broadcast lockout durations to their proper values * Added AI vision wire to holopads * Bug fixes * AI vision and interaction wires can be added to the same wire panel * Fixed error * More bug fixes * Fixed test fail * Embellished the emergency call lock out window * Holopads play borg sounds when speaking * Borg and AI names are listed as the caller ID on the holopad * Borg chassis can now be seen on holopad holograms * Holopad returns to a machine frame when badly damaged * Clarified some text * Fix merge conflict * Fixed merge conflict * Fixing merge conflict * Fixing merge conflict * Fixing merge conflict * Offset menu on open * AI can alt click on holopads to activate the projector * Bug fixes for intellicard interactions * Fixed speech issue with intellicards * The UI automatically opens for the AI when it alt-clicks on the holopad * Simplified shader math * Telephones will auto hang up 60 seconds after the last person on a call stops speaking * Added better support for AI requests when multiple AI cores are on the station * The call controls pop up for the AI when they accept a summons from a holopad * Compatibility mode fix for the hologram shader * Further shader fixes for compatibility mode * File clean up * More cleaning up * Removed access requirements from quantum holopads so they can used by nukies * The title of the holopad window now reflects the name of the device * Linked telephones will lose their connection if both move out of range of each other
This commit is contained in:
761
Content.Server/Holopad/HolopadSystem.cs
Normal file
761
Content.Server/Holopad/HolopadSystem.cs
Normal file
@@ -0,0 +1,761 @@
|
||||
using Content.Server.Chat.Systems;
|
||||
using Content.Server.Power.EntitySystems;
|
||||
using Content.Server.Speech.Components;
|
||||
using Content.Server.Telephone;
|
||||
using Content.Shared.Access.Systems;
|
||||
using Content.Shared.Audio;
|
||||
using Content.Shared.Chat.TypingIndicator;
|
||||
using Content.Shared.Holopad;
|
||||
using Content.Shared.IdentityManagement;
|
||||
using Content.Shared.Labels.Components;
|
||||
using Content.Shared.Silicons.StationAi;
|
||||
using Content.Shared.Telephone;
|
||||
using Content.Shared.UserInterface;
|
||||
using Content.Shared.Verbs;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
using System.Linq;
|
||||
|
||||
namespace Content.Server.Holopad;
|
||||
|
||||
public sealed class HolopadSystem : SharedHolopadSystem
|
||||
{
|
||||
[Dependency] private readonly TelephoneSystem _telephoneSystem = default!;
|
||||
[Dependency] private readonly UserInterfaceSystem _userInterfaceSystem = default!;
|
||||
[Dependency] private readonly TransformSystem _xformSystem = default!;
|
||||
[Dependency] private readonly AppearanceSystem _appearanceSystem = default!;
|
||||
[Dependency] private readonly SharedPointLightSystem _pointLightSystem = default!;
|
||||
[Dependency] private readonly SharedAmbientSoundSystem _ambientSoundSystem = default!;
|
||||
[Dependency] private readonly SharedStationAiSystem _stationAiSystem = default!;
|
||||
[Dependency] private readonly AccessReaderSystem _accessReaderSystem = default!;
|
||||
[Dependency] private readonly ChatSystem _chatSystem = default!;
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
|
||||
private float _updateTimer = 1.0f;
|
||||
|
||||
private const float UpdateTime = 1.0f;
|
||||
private const float MinTimeBetweenSyncRequests = 0.5f;
|
||||
private TimeSpan _minTimeSpanBetweenSyncRequests;
|
||||
|
||||
private HashSet<EntityUid> _pendingRequestsForSpriteState = new();
|
||||
private HashSet<EntityUid> _recentlyUpdatedHolograms = new();
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
_minTimeSpanBetweenSyncRequests = TimeSpan.FromSeconds(MinTimeBetweenSyncRequests);
|
||||
|
||||
// Holopad UI and bound user interface messages
|
||||
SubscribeLocalEvent<HolopadComponent, BeforeActivatableUIOpenEvent>(OnUIOpen);
|
||||
SubscribeLocalEvent<HolopadComponent, HolopadStartNewCallMessage>(OnHolopadStartNewCall);
|
||||
SubscribeLocalEvent<HolopadComponent, HolopadAnswerCallMessage>(OnHolopadAnswerCall);
|
||||
SubscribeLocalEvent<HolopadComponent, HolopadEndCallMessage>(OnHolopadEndCall);
|
||||
SubscribeLocalEvent<HolopadComponent, HolopadActivateProjectorMessage>(OnHolopadActivateProjector);
|
||||
SubscribeLocalEvent<HolopadComponent, HolopadStartBroadcastMessage>(OnHolopadStartBroadcast);
|
||||
SubscribeLocalEvent<HolopadComponent, HolopadStationAiRequestMessage>(OnHolopadStationAiRequest);
|
||||
|
||||
// Holopad telephone events
|
||||
SubscribeLocalEvent<HolopadComponent, TelephoneStateChangeEvent>(OnTelephoneStateChange);
|
||||
SubscribeLocalEvent<HolopadComponent, TelephoneCallCommencedEvent>(OnHoloCallCommenced);
|
||||
SubscribeLocalEvent<HolopadComponent, TelephoneCallEndedEvent>(OnHoloCallEnded);
|
||||
SubscribeLocalEvent<HolopadComponent, TelephoneMessageSentEvent>(OnTelephoneMessageSent);
|
||||
|
||||
// Networked events
|
||||
SubscribeNetworkEvent<HolopadUserTypingChangedEvent>(OnTypingChanged);
|
||||
SubscribeNetworkEvent<PlayerSpriteStateMessage>(OnPlayerSpriteStateMessage);
|
||||
|
||||
// Component start/shutdown events
|
||||
SubscribeLocalEvent<HolopadComponent, ComponentInit>(OnHolopadInit);
|
||||
SubscribeLocalEvent<HolopadComponent, ComponentShutdown>(OnHolopadShutdown);
|
||||
SubscribeLocalEvent<HolopadUserComponent, ComponentInit>(OnHolopadUserInit);
|
||||
SubscribeLocalEvent<HolopadUserComponent, ComponentShutdown>(OnHolopadUserShutdown);
|
||||
|
||||
// Misc events
|
||||
SubscribeLocalEvent<HolopadUserComponent, EmoteEvent>(OnEmote);
|
||||
SubscribeLocalEvent<HolopadUserComponent, JumpToCoreEvent>(OnJumpToCore);
|
||||
SubscribeLocalEvent<HolopadComponent, GetVerbsEvent<AlternativeVerb>>(AddToggleProjectorVerb);
|
||||
SubscribeLocalEvent<HolopadComponent, EntRemovedFromContainerMessage>(OnAiRemove);
|
||||
}
|
||||
|
||||
#region: Holopad UI bound user interface messages
|
||||
|
||||
private void OnUIOpen(Entity<HolopadComponent> entity, ref BeforeActivatableUIOpenEvent args)
|
||||
{
|
||||
UpdateUIState(entity);
|
||||
}
|
||||
|
||||
private void OnHolopadStartNewCall(Entity<HolopadComponent> source, ref HolopadStartNewCallMessage args)
|
||||
{
|
||||
if (IsHolopadControlLocked(source, args.Actor))
|
||||
return;
|
||||
|
||||
if (!TryComp<TelephoneComponent>(source, out var sourceTelephone))
|
||||
return;
|
||||
|
||||
var receiver = GetEntity(args.Receiver);
|
||||
|
||||
if (!TryComp<TelephoneComponent>(receiver, out var receiverTelephone))
|
||||
return;
|
||||
|
||||
LinkHolopadToUser(source, args.Actor);
|
||||
_telephoneSystem.CallTelephone((source, sourceTelephone), (receiver, receiverTelephone), args.Actor);
|
||||
}
|
||||
|
||||
private void OnHolopadAnswerCall(Entity<HolopadComponent> receiver, ref HolopadAnswerCallMessage args)
|
||||
{
|
||||
if (IsHolopadControlLocked(receiver, args.Actor))
|
||||
return;
|
||||
|
||||
if (!TryComp<TelephoneComponent>(receiver, out var receiverTelephone))
|
||||
return;
|
||||
|
||||
if (TryComp<StationAiHeldComponent>(args.Actor, out var userAiHeld))
|
||||
{
|
||||
var source = GetLinkedHolopads(receiver).FirstOrNull();
|
||||
|
||||
if (source != null)
|
||||
ActivateProjector(source.Value, args.Actor);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
LinkHolopadToUser(receiver, args.Actor);
|
||||
_telephoneSystem.AnswerTelephone((receiver, receiverTelephone), args.Actor);
|
||||
}
|
||||
|
||||
private void OnHolopadEndCall(Entity<HolopadComponent> entity, ref HolopadEndCallMessage args)
|
||||
{
|
||||
if (!TryComp<TelephoneComponent>(entity, out var entityTelephone))
|
||||
return;
|
||||
|
||||
if (IsHolopadControlLocked(entity, args.Actor))
|
||||
return;
|
||||
|
||||
_telephoneSystem.EndTelephoneCalls((entity, entityTelephone));
|
||||
|
||||
// If the user is an AI, end all calls originating from its
|
||||
// associated core to ensure that any broadcasts will end
|
||||
if (!TryComp<StationAiHeldComponent>(args.Actor, out var stationAiHeld) ||
|
||||
!_stationAiSystem.TryGetStationAiCore((args.Actor, stationAiHeld), out var stationAiCore))
|
||||
return;
|
||||
|
||||
if (TryComp<TelephoneComponent>(stationAiCore, out var telephone))
|
||||
_telephoneSystem.EndTelephoneCalls((stationAiCore.Value, telephone));
|
||||
}
|
||||
|
||||
private void OnHolopadActivateProjector(Entity<HolopadComponent> entity, ref HolopadActivateProjectorMessage args)
|
||||
{
|
||||
ActivateProjector(entity, args.Actor);
|
||||
}
|
||||
|
||||
private void OnHolopadStartBroadcast(Entity<HolopadComponent> source, ref HolopadStartBroadcastMessage args)
|
||||
{
|
||||
if (IsHolopadControlLocked(source, args.Actor) || IsHolopadBroadcastOnCoolDown(source))
|
||||
return;
|
||||
|
||||
if (!_accessReaderSystem.IsAllowed(args.Actor, source))
|
||||
return;
|
||||
|
||||
// AI broadcasting
|
||||
if (TryComp<StationAiHeldComponent>(args.Actor, out var stationAiHeld))
|
||||
{
|
||||
if (!_stationAiSystem.TryGetStationAiCore((args.Actor, stationAiHeld), out var stationAiCore) ||
|
||||
stationAiCore.Value.Comp.RemoteEntity == null ||
|
||||
!TryComp<HolopadComponent>(stationAiCore, out var stationAiCoreHolopad))
|
||||
return;
|
||||
|
||||
ExecuteBroadcast((stationAiCore.Value, stationAiCoreHolopad), args.Actor);
|
||||
|
||||
// Switch the AI's perspective from free roaming to the target holopad
|
||||
_xformSystem.SetCoordinates(stationAiCore.Value.Comp.RemoteEntity.Value, Transform(source).Coordinates);
|
||||
_stationAiSystem.SwitchRemoteEntityMode(stationAiCore.Value, false);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Crew broadcasting
|
||||
ExecuteBroadcast(source, args.Actor);
|
||||
}
|
||||
|
||||
private void OnHolopadStationAiRequest(Entity<HolopadComponent> entity, ref HolopadStationAiRequestMessage args)
|
||||
{
|
||||
if (IsHolopadControlLocked(entity, args.Actor))
|
||||
return;
|
||||
|
||||
if (!TryComp<TelephoneComponent>(entity, out var telephone))
|
||||
return;
|
||||
|
||||
var source = new Entity<TelephoneComponent>(entity, telephone);
|
||||
var query = AllEntityQuery<StationAiCoreComponent, TelephoneComponent>();
|
||||
var reachableAiCores = new HashSet<Entity<TelephoneComponent>>();
|
||||
|
||||
while (query.MoveNext(out var receiverUid, out var receiverStationAiCore, out var receiverTelephone))
|
||||
{
|
||||
var receiver = new Entity<TelephoneComponent>(receiverUid, receiverTelephone);
|
||||
|
||||
if (!_telephoneSystem.IsSourceAbleToReachReceiver(source, receiver))
|
||||
continue;
|
||||
|
||||
if (_telephoneSystem.IsTelephoneEngaged(receiver))
|
||||
continue;
|
||||
|
||||
reachableAiCores.Add((receiverUid, receiverTelephone));
|
||||
|
||||
if (!_stationAiSystem.TryGetInsertedAI((receiver, receiverStationAiCore), out var insertedAi))
|
||||
continue;
|
||||
|
||||
if (_userInterfaceSystem.TryOpenUi(receiverUid, HolopadUiKey.AiRequestWindow, insertedAi.Value.Owner))
|
||||
LinkHolopadToUser(entity, args.Actor);
|
||||
}
|
||||
|
||||
if (!reachableAiCores.Any())
|
||||
return;
|
||||
|
||||
_telephoneSystem.BroadcastCallToTelephones(source, reachableAiCores, args.Actor);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region: Holopad telephone events
|
||||
|
||||
private void OnTelephoneStateChange(Entity<HolopadComponent> holopad, ref TelephoneStateChangeEvent args)
|
||||
{
|
||||
// Update holopad visual and ambient states
|
||||
switch (args.NewState)
|
||||
{
|
||||
case TelephoneState.Idle:
|
||||
ShutDownHolopad(holopad);
|
||||
SetHolopadAmbientState(holopad, false);
|
||||
break;
|
||||
|
||||
case TelephoneState.EndingCall:
|
||||
ShutDownHolopad(holopad);
|
||||
break;
|
||||
|
||||
default:
|
||||
SetHolopadAmbientState(holopad, this.IsPowered(holopad, EntityManager));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnHoloCallCommenced(Entity<HolopadComponent> source, ref TelephoneCallCommencedEvent args)
|
||||
{
|
||||
if (source.Comp.Hologram == null)
|
||||
GenerateHologram(source);
|
||||
|
||||
// Receiver holopad holograms have to be generated now instead of waiting for their own event
|
||||
// to fire because holographic avatars get synced immediately
|
||||
if (TryComp<HolopadComponent>(args.Receiver, out var receivingHolopad) && receivingHolopad.Hologram == null)
|
||||
GenerateHologram((args.Receiver, receivingHolopad));
|
||||
|
||||
if (source.Comp.User != null)
|
||||
{
|
||||
// Re-link the user to refresh the sprite data
|
||||
LinkHolopadToUser(source, source.Comp.User.Value);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnHoloCallEnded(Entity<HolopadComponent> entity, ref TelephoneCallEndedEvent args)
|
||||
{
|
||||
if (!TryComp<StationAiCoreComponent>(entity, out var stationAiCore))
|
||||
return;
|
||||
|
||||
// Auto-close the AI request window
|
||||
if (_stationAiSystem.TryGetInsertedAI((entity, stationAiCore), out var insertedAi))
|
||||
_userInterfaceSystem.CloseUi(entity.Owner, HolopadUiKey.AiRequestWindow, insertedAi.Value.Owner);
|
||||
}
|
||||
|
||||
private void OnTelephoneMessageSent(Entity<HolopadComponent> holopad, ref TelephoneMessageSentEvent args)
|
||||
{
|
||||
LinkHolopadToUser(holopad, args.MessageSource);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region: Networked events
|
||||
|
||||
private void OnTypingChanged(HolopadUserTypingChangedEvent ev, EntitySessionEventArgs args)
|
||||
{
|
||||
var uid = args.SenderSession.AttachedEntity;
|
||||
|
||||
if (!Exists(uid))
|
||||
return;
|
||||
|
||||
if (!TryComp<HolopadUserComponent>(uid, out var holopadUser))
|
||||
return;
|
||||
|
||||
foreach (var linkedHolopad in holopadUser.LinkedHolopads)
|
||||
{
|
||||
var receiverHolopads = GetLinkedHolopads(linkedHolopad);
|
||||
|
||||
foreach (var receiverHolopad in receiverHolopads)
|
||||
{
|
||||
if (receiverHolopad.Comp.Hologram == null)
|
||||
continue;
|
||||
|
||||
_appearanceSystem.SetData(receiverHolopad.Comp.Hologram.Value.Owner, TypingIndicatorVisuals.IsTyping, ev.IsTyping);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnPlayerSpriteStateMessage(PlayerSpriteStateMessage ev, EntitySessionEventArgs args)
|
||||
{
|
||||
var uid = args.SenderSession.AttachedEntity;
|
||||
|
||||
if (!Exists(uid))
|
||||
return;
|
||||
|
||||
if (!_pendingRequestsForSpriteState.Remove(uid.Value))
|
||||
return;
|
||||
|
||||
if (!TryComp<HolopadUserComponent>(uid, out var holopadUser))
|
||||
return;
|
||||
|
||||
SyncHolopadUserWithLinkedHolograms((uid.Value, holopadUser), ev.SpriteLayerData);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region: Component start/shutdown events
|
||||
|
||||
private void OnHolopadInit(Entity<HolopadComponent> entity, ref ComponentInit args)
|
||||
{
|
||||
if (entity.Comp.User != null)
|
||||
LinkHolopadToUser(entity, entity.Comp.User.Value);
|
||||
}
|
||||
|
||||
private void OnHolopadUserInit(Entity<HolopadUserComponent> entity, ref ComponentInit args)
|
||||
{
|
||||
foreach (var linkedHolopad in entity.Comp.LinkedHolopads)
|
||||
LinkHolopadToUser(linkedHolopad, entity);
|
||||
}
|
||||
|
||||
private void OnHolopadShutdown(Entity<HolopadComponent> entity, ref ComponentShutdown args)
|
||||
{
|
||||
ShutDownHolopad(entity);
|
||||
SetHolopadAmbientState(entity, false);
|
||||
}
|
||||
|
||||
private void OnHolopadUserShutdown(Entity<HolopadUserComponent> entity, ref ComponentShutdown args)
|
||||
{
|
||||
foreach (var linkedHolopad in entity.Comp.LinkedHolopads)
|
||||
UnlinkHolopadFromUser(linkedHolopad, entity);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region: Misc events
|
||||
|
||||
private void OnEmote(Entity<HolopadUserComponent> entity, ref EmoteEvent args)
|
||||
{
|
||||
foreach (var linkedHolopad in entity.Comp.LinkedHolopads)
|
||||
{
|
||||
// Treat the ability to hear speech as the ability to also perceive emotes
|
||||
// (these are almost always going to be linked)
|
||||
if (!HasComp<ActiveListenerComponent>(linkedHolopad))
|
||||
continue;
|
||||
|
||||
if (TryComp<TelephoneComponent>(linkedHolopad, out var linkedHolopadTelephone) && linkedHolopadTelephone.Muted)
|
||||
continue;
|
||||
|
||||
foreach (var receiver in GetLinkedHolopads(linkedHolopad))
|
||||
{
|
||||
if (receiver.Comp.Hologram == null)
|
||||
continue;
|
||||
|
||||
// Name is based on the physical identity of the user
|
||||
var ent = Identity.Entity(entity, EntityManager);
|
||||
var name = Loc.GetString("holopad-hologram-name", ("name", ent));
|
||||
|
||||
// Force the emote, because if the user can do it, the hologram can too
|
||||
_chatSystem.TryEmoteWithChat(receiver.Comp.Hologram.Value, args.Emote, ChatTransmitRange.Normal, false, name, true, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnJumpToCore(Entity<HolopadUserComponent> entity, ref JumpToCoreEvent args)
|
||||
{
|
||||
if (!TryComp<StationAiHeldComponent>(entity, out var entityStationAiHeld))
|
||||
return;
|
||||
|
||||
if (!_stationAiSystem.TryGetStationAiCore((entity, entityStationAiHeld), out var stationAiCore))
|
||||
return;
|
||||
|
||||
if (!TryComp<TelephoneComponent>(stationAiCore, out var stationAiCoreTelephone))
|
||||
return;
|
||||
|
||||
_telephoneSystem.EndTelephoneCalls((stationAiCore.Value, stationAiCoreTelephone));
|
||||
}
|
||||
|
||||
private void AddToggleProjectorVerb(Entity<HolopadComponent> entity, ref GetVerbsEvent<AlternativeVerb> args)
|
||||
{
|
||||
if (!args.CanAccess || !args.CanInteract)
|
||||
return;
|
||||
|
||||
if (!this.IsPowered(entity, EntityManager))
|
||||
return;
|
||||
|
||||
if (!TryComp<TelephoneComponent>(entity, out var entityTelephone) ||
|
||||
_telephoneSystem.IsTelephoneEngaged((entity, entityTelephone)))
|
||||
return;
|
||||
|
||||
var user = args.User;
|
||||
|
||||
if (!TryComp<StationAiHeldComponent>(user, out var userAiHeld))
|
||||
return;
|
||||
|
||||
if (!_stationAiSystem.TryGetStationAiCore((user, userAiHeld), out var stationAiCore) ||
|
||||
stationAiCore.Value.Comp.RemoteEntity == null)
|
||||
return;
|
||||
|
||||
AlternativeVerb verb = new()
|
||||
{
|
||||
Act = () => ActivateProjector(entity, user),
|
||||
Text = Loc.GetString("activate-holopad-projector-verb"),
|
||||
Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/vv.svg.192dpi.png")),
|
||||
};
|
||||
|
||||
args.Verbs.Add(verb);
|
||||
}
|
||||
|
||||
private void OnAiRemove(Entity<HolopadComponent> entity, ref EntRemovedFromContainerMessage args)
|
||||
{
|
||||
if (!HasComp<StationAiCoreComponent>(entity))
|
||||
return;
|
||||
|
||||
if (!TryComp<TelephoneComponent>(entity, out var entityTelephone))
|
||||
return;
|
||||
|
||||
_telephoneSystem.EndTelephoneCalls((entity, entityTelephone));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
base.Update(frameTime);
|
||||
|
||||
_updateTimer += frameTime;
|
||||
|
||||
if (_updateTimer >= UpdateTime)
|
||||
{
|
||||
_updateTimer -= UpdateTime;
|
||||
|
||||
var query = AllEntityQuery<HolopadComponent, TelephoneComponent, TransformComponent>();
|
||||
while (query.MoveNext(out var uid, out var holopad, out var telephone, out var xform))
|
||||
{
|
||||
UpdateUIState((uid, holopad), telephone);
|
||||
|
||||
if (holopad.User != null &&
|
||||
!HasComp<IgnoreUIRangeComponent>(holopad.User) &&
|
||||
!_xformSystem.InRange((holopad.User.Value, Transform(holopad.User.Value)), (uid, xform), telephone.ListeningRange))
|
||||
{
|
||||
UnlinkHolopadFromUser((uid, holopad), holopad.User.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_recentlyUpdatedHolograms.Clear();
|
||||
}
|
||||
|
||||
public void UpdateUIState(Entity<HolopadComponent> entity, TelephoneComponent? telephone = null)
|
||||
{
|
||||
if (!Resolve(entity.Owner, ref telephone, false))
|
||||
return;
|
||||
|
||||
var source = new Entity<TelephoneComponent>(entity, telephone);
|
||||
var holopads = new Dictionary<NetEntity, string>();
|
||||
|
||||
var query = AllEntityQuery<HolopadComponent, TelephoneComponent>();
|
||||
while (query.MoveNext(out var receiverUid, out var _, out var receiverTelephone))
|
||||
{
|
||||
var receiver = new Entity<TelephoneComponent>(receiverUid, receiverTelephone);
|
||||
|
||||
if (receiverTelephone.UnlistedNumber)
|
||||
continue;
|
||||
|
||||
if (source == receiver)
|
||||
continue;
|
||||
|
||||
if (!_telephoneSystem.IsSourceInRangeOfReceiver(source, receiver))
|
||||
continue;
|
||||
|
||||
var name = MetaData(receiverUid).EntityName;
|
||||
|
||||
if (TryComp<LabelComponent>(receiverUid, out var label) && !string.IsNullOrEmpty(label.CurrentLabel))
|
||||
name = label.CurrentLabel;
|
||||
|
||||
holopads.Add(GetNetEntity(receiverUid), name);
|
||||
}
|
||||
|
||||
var uiKey = HasComp<StationAiCoreComponent>(entity) ? HolopadUiKey.AiActionWindow : HolopadUiKey.InteractionWindow;
|
||||
_userInterfaceSystem.SetUiState(entity.Owner, uiKey, new HolopadBoundInterfaceState(holopads));
|
||||
}
|
||||
|
||||
private void GenerateHologram(Entity<HolopadComponent> entity)
|
||||
{
|
||||
if (entity.Comp.Hologram != null ||
|
||||
entity.Comp.HologramProtoId == null)
|
||||
return;
|
||||
|
||||
var uid = Spawn(entity.Comp.HologramProtoId, Transform(entity).Coordinates);
|
||||
|
||||
// Safeguard - spawned holograms must have this component
|
||||
if (!TryComp<HolopadHologramComponent>(uid, out var component))
|
||||
{
|
||||
Del(uid);
|
||||
return;
|
||||
}
|
||||
|
||||
entity.Comp.Hologram = new Entity<HolopadHologramComponent>(uid, component);
|
||||
}
|
||||
|
||||
private void DeleteHologram(Entity<HolopadHologramComponent> hologram, Entity<HolopadComponent> attachedHolopad)
|
||||
{
|
||||
attachedHolopad.Comp.Hologram = null;
|
||||
|
||||
QueueDel(hologram);
|
||||
}
|
||||
|
||||
private void LinkHolopadToUser(Entity<HolopadComponent> entity, EntityUid user)
|
||||
{
|
||||
if (!TryComp<HolopadUserComponent>(user, out var holopadUser))
|
||||
holopadUser = AddComp<HolopadUserComponent>(user);
|
||||
|
||||
if (user != entity.Comp.User?.Owner)
|
||||
{
|
||||
// Removes the old user from the holopad
|
||||
UnlinkHolopadFromUser(entity, entity.Comp.User);
|
||||
|
||||
// Assigns the new user in their place
|
||||
holopadUser.LinkedHolopads.Add(entity);
|
||||
entity.Comp.User = (user, holopadUser);
|
||||
}
|
||||
|
||||
if (TryComp<HolographicAvatarComponent>(user, out var avatar))
|
||||
{
|
||||
SyncHolopadUserWithLinkedHolograms((user, holopadUser), avatar.LayerData);
|
||||
return;
|
||||
}
|
||||
|
||||
// We have no apriori sprite data for the hologram, request
|
||||
// the current appearance of the user from the client
|
||||
RequestHolopadUserSpriteUpdate((user, holopadUser));
|
||||
}
|
||||
|
||||
private void UnlinkHolopadFromUser(Entity<HolopadComponent> entity, Entity<HolopadUserComponent>? user)
|
||||
{
|
||||
if (user == null)
|
||||
return;
|
||||
|
||||
entity.Comp.User = null;
|
||||
|
||||
foreach (var linkedHolopad in GetLinkedHolopads(entity))
|
||||
{
|
||||
if (linkedHolopad.Comp.Hologram != null)
|
||||
{
|
||||
_appearanceSystem.SetData(linkedHolopad.Comp.Hologram.Value.Owner, TypingIndicatorVisuals.IsTyping, false);
|
||||
|
||||
// Send message with no sprite data to the client
|
||||
// This will set the holgram sprite to a generic icon
|
||||
var ev = new PlayerSpriteStateMessage(GetNetEntity(linkedHolopad.Comp.Hologram.Value));
|
||||
RaiseNetworkEvent(ev);
|
||||
}
|
||||
}
|
||||
|
||||
if (!HasComp<HolopadUserComponent>(user))
|
||||
return;
|
||||
|
||||
user.Value.Comp.LinkedHolopads.Remove(entity);
|
||||
|
||||
if (!user.Value.Comp.LinkedHolopads.Any())
|
||||
{
|
||||
_pendingRequestsForSpriteState.Remove(user.Value);
|
||||
|
||||
if (user.Value.Comp.LifeStage < ComponentLifeStage.Stopping)
|
||||
RemComp<HolopadUserComponent>(user.Value);
|
||||
}
|
||||
}
|
||||
|
||||
private void ShutDownHolopad(Entity<HolopadComponent> entity)
|
||||
{
|
||||
entity.Comp.ControlLockoutOwner = null;
|
||||
|
||||
if (entity.Comp.Hologram != null)
|
||||
DeleteHologram(entity.Comp.Hologram.Value, entity);
|
||||
|
||||
if (entity.Comp.User != null)
|
||||
UnlinkHolopadFromUser(entity, entity.Comp.User.Value);
|
||||
|
||||
if (TryComp<StationAiCoreComponent>(entity, out var stationAiCore))
|
||||
{
|
||||
_stationAiSystem.SwitchRemoteEntityMode((entity.Owner, stationAiCore), true);
|
||||
|
||||
if (TryComp<TelephoneComponent>(entity, out var stationAiCoreTelphone))
|
||||
_telephoneSystem.EndTelephoneCalls((entity, stationAiCoreTelphone));
|
||||
}
|
||||
|
||||
Dirty(entity);
|
||||
}
|
||||
|
||||
private void RequestHolopadUserSpriteUpdate(Entity<HolopadUserComponent> user)
|
||||
{
|
||||
if (!_pendingRequestsForSpriteState.Add(user))
|
||||
return;
|
||||
|
||||
var ev = new PlayerSpriteStateRequest(GetNetEntity(user));
|
||||
RaiseNetworkEvent(ev);
|
||||
}
|
||||
|
||||
private void SyncHolopadUserWithLinkedHolograms(Entity<HolopadUserComponent> entity, PrototypeLayerData[]? spriteLayerData)
|
||||
{
|
||||
foreach (var linkedHolopad in entity.Comp.LinkedHolopads)
|
||||
{
|
||||
foreach (var receivingHolopad in GetLinkedHolopads(linkedHolopad))
|
||||
{
|
||||
if (receivingHolopad.Comp.Hologram == null || !_recentlyUpdatedHolograms.Add(receivingHolopad.Comp.Hologram.Value))
|
||||
continue;
|
||||
|
||||
var netHologram = GetNetEntity(receivingHolopad.Comp.Hologram.Value);
|
||||
var ev = new PlayerSpriteStateMessage(netHologram, spriteLayerData);
|
||||
RaiseNetworkEvent(ev);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ActivateProjector(Entity<HolopadComponent> entity, EntityUid user)
|
||||
{
|
||||
if (!TryComp<TelephoneComponent>(entity, out var receiverTelephone))
|
||||
return;
|
||||
|
||||
var receiver = new Entity<TelephoneComponent>(entity, receiverTelephone);
|
||||
|
||||
if (!TryComp<StationAiHeldComponent>(user, out var userAiHeld))
|
||||
return;
|
||||
|
||||
if (!_stationAiSystem.TryGetStationAiCore((user, userAiHeld), out var stationAiCore) ||
|
||||
stationAiCore.Value.Comp.RemoteEntity == null)
|
||||
return;
|
||||
|
||||
if (!TryComp<TelephoneComponent>(stationAiCore, out var stationAiTelephone))
|
||||
return;
|
||||
|
||||
if (!TryComp<HolopadComponent>(stationAiCore, out var stationAiHolopad))
|
||||
return;
|
||||
|
||||
var source = new Entity<TelephoneComponent>(stationAiCore.Value, stationAiTelephone);
|
||||
|
||||
// Terminate any calls that the core is hosting and immediately connect to the receiver
|
||||
_telephoneSystem.TerminateTelephoneCalls(source);
|
||||
|
||||
var callOptions = new TelephoneCallOptions()
|
||||
{
|
||||
ForceConnect = true,
|
||||
MuteReceiver = true
|
||||
};
|
||||
|
||||
_telephoneSystem.CallTelephone(source, receiver, user, callOptions);
|
||||
|
||||
if (!_telephoneSystem.IsSourceConnectedToReceiver(source, receiver))
|
||||
return;
|
||||
|
||||
LinkHolopadToUser((stationAiCore.Value, stationAiHolopad), user);
|
||||
|
||||
// Switch the AI's perspective from free roaming to the target holopad
|
||||
_xformSystem.SetCoordinates(stationAiCore.Value.Comp.RemoteEntity.Value, Transform(entity).Coordinates);
|
||||
_stationAiSystem.SwitchRemoteEntityMode(stationAiCore.Value, false);
|
||||
|
||||
// Open the holopad UI if it hasn't been opened yet
|
||||
if (TryComp<UserInterfaceComponent>(entity, out var entityUserInterfaceComponent))
|
||||
_userInterfaceSystem.OpenUi((entity, entityUserInterfaceComponent), HolopadUiKey.InteractionWindow, user);
|
||||
}
|
||||
|
||||
private void ExecuteBroadcast(Entity<HolopadComponent> source, EntityUid user)
|
||||
{
|
||||
if (!TryComp<TelephoneComponent>(source, out var sourceTelephone))
|
||||
return;
|
||||
|
||||
var sourceTelephoneEntity = new Entity<TelephoneComponent>(source, sourceTelephone);
|
||||
_telephoneSystem.TerminateTelephoneCalls(sourceTelephoneEntity);
|
||||
|
||||
// Find all holopads in range of the source
|
||||
var sourceXform = Transform(source);
|
||||
var receivers = new HashSet<Entity<TelephoneComponent>>();
|
||||
|
||||
var query = AllEntityQuery<HolopadComponent, TelephoneComponent, TransformComponent>();
|
||||
while (query.MoveNext(out var receiver, out var receiverHolopad, out var receiverTelephone, out var receiverXform))
|
||||
{
|
||||
var receiverTelephoneEntity = new Entity<TelephoneComponent>(receiver, receiverTelephone);
|
||||
|
||||
if (sourceTelephoneEntity == receiverTelephoneEntity ||
|
||||
receiverTelephone.UnlistedNumber ||
|
||||
!_telephoneSystem.IsSourceAbleToReachReceiver(sourceTelephoneEntity, receiverTelephoneEntity))
|
||||
continue;
|
||||
|
||||
// If any holopads in range are on broadcast cooldown, exit
|
||||
if (IsHolopadBroadcastOnCoolDown((receiver, receiverHolopad)))
|
||||
return;
|
||||
|
||||
receivers.Add(receiverTelephoneEntity);
|
||||
}
|
||||
|
||||
var options = new TelephoneCallOptions()
|
||||
{
|
||||
ForceConnect = true,
|
||||
MuteReceiver = true,
|
||||
};
|
||||
|
||||
_telephoneSystem.BroadcastCallToTelephones(sourceTelephoneEntity, receivers, user, options);
|
||||
|
||||
if (!_telephoneSystem.IsTelephoneEngaged(sourceTelephoneEntity))
|
||||
return;
|
||||
|
||||
// Link to the user after all the calls have been placed,
|
||||
// so we only need to sync all the holograms once
|
||||
LinkHolopadToUser(source, user);
|
||||
|
||||
// Lock out the controls of all involved holopads for a set duration
|
||||
source.Comp.ControlLockoutOwner = user;
|
||||
source.Comp.ControlLockoutStartTime = _timing.CurTime;
|
||||
|
||||
Dirty(source);
|
||||
|
||||
foreach (var receiver in GetLinkedHolopads(source))
|
||||
{
|
||||
receiver.Comp.ControlLockoutOwner = user;
|
||||
receiver.Comp.ControlLockoutStartTime = _timing.CurTime;
|
||||
|
||||
Dirty(receiver);
|
||||
}
|
||||
}
|
||||
|
||||
private HashSet<Entity<HolopadComponent>> GetLinkedHolopads(Entity<HolopadComponent> entity)
|
||||
{
|
||||
var linkedHolopads = new HashSet<Entity<HolopadComponent>>();
|
||||
|
||||
if (!TryComp<TelephoneComponent>(entity, out var holopadTelephone))
|
||||
return linkedHolopads;
|
||||
|
||||
foreach (var linkedEnt in holopadTelephone.LinkedTelephones)
|
||||
{
|
||||
if (!TryComp<HolopadComponent>(linkedEnt, out var linkedHolopad))
|
||||
continue;
|
||||
|
||||
linkedHolopads.Add((linkedEnt, linkedHolopad));
|
||||
}
|
||||
|
||||
return linkedHolopads;
|
||||
}
|
||||
|
||||
private void SetHolopadAmbientState(Entity<HolopadComponent> entity, bool isEnabled)
|
||||
{
|
||||
if (TryComp<PointLightComponent>(entity, out var pointLight))
|
||||
_pointLightSystem.SetEnabled(entity, isEnabled, pointLight);
|
||||
|
||||
if (TryComp<AmbientSoundComponent>(entity, out var ambientSound))
|
||||
_ambientSoundSystem.SetAmbience(entity, isEnabled, ambientSound);
|
||||
}
|
||||
}
|
||||
@@ -12,8 +12,8 @@ namespace Content.Server.Silicons.StationAi;
|
||||
public sealed partial class AiVisionWireAction : ComponentWireAction<StationAiVisionComponent>
|
||||
{
|
||||
public override string Name { get; set; } = "wire-name-ai-vision-light";
|
||||
public override Color Color { get; set; } = Color.DeepSkyBlue;
|
||||
public override object StatusKey => AirlockWireStatus.AiControlIndicator;
|
||||
public override Color Color { get; set; } = Color.White;
|
||||
public override object StatusKey => AirlockWireStatus.AiVisionIndicator;
|
||||
|
||||
public override StatusLightState? GetLightState(Wire wire, StationAiVisionComponent component)
|
||||
{
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Linq;
|
||||
using Content.Server.Chat.Managers;
|
||||
using Content.Server.Chat.Systems;
|
||||
using Content.Shared.Chat;
|
||||
using Content.Shared.Mind;
|
||||
using Content.Shared.Roles;
|
||||
@@ -8,6 +9,7 @@ using Content.Shared.StationAi;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Map.Components;
|
||||
using Robust.Shared.Player;
|
||||
using static Content.Server.Chat.Systems.ChatSystem;
|
||||
|
||||
namespace Content.Server.Silicons.StationAi;
|
||||
|
||||
@@ -15,11 +17,50 @@ public sealed class StationAiSystem : SharedStationAiSystem
|
||||
{
|
||||
[Dependency] private readonly IChatManager _chats = default!;
|
||||
[Dependency] private readonly EntityLookupSystem _lookup = default!;
|
||||
[Dependency] private readonly SharedTransformSystem _xforms = default!;
|
||||
[Dependency] private readonly SharedMindSystem _mind = default!;
|
||||
[Dependency] private readonly SharedRoleSystem _roles = default!;
|
||||
|
||||
private readonly HashSet<Entity<StationAiCoreComponent>> _ais = new();
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<ExpandICChatRecipientsEvent>(OnExpandICChatRecipients);
|
||||
}
|
||||
|
||||
private void OnExpandICChatRecipients(ExpandICChatRecipientsEvent ev)
|
||||
{
|
||||
var xformQuery = GetEntityQuery<TransformComponent>();
|
||||
var sourceXform = Transform(ev.Source);
|
||||
var sourcePos = _xforms.GetWorldPosition(sourceXform, xformQuery);
|
||||
|
||||
// This function ensures that chat popups appear on camera views that have connected microphones.
|
||||
var query = EntityManager.EntityQueryEnumerator<StationAiCoreComponent, TransformComponent>();
|
||||
while (query.MoveNext(out var ent, out var entStationAiCore, out var entXform))
|
||||
{
|
||||
var stationAiCore = new Entity<StationAiCoreComponent>(ent, entStationAiCore);
|
||||
|
||||
if (!TryGetInsertedAI(stationAiCore, out var insertedAi) || !TryComp(insertedAi, out ActorComponent? actor))
|
||||
return;
|
||||
|
||||
if (stationAiCore.Comp.RemoteEntity == null || stationAiCore.Comp.Remote)
|
||||
return;
|
||||
|
||||
var xform = Transform(stationAiCore.Comp.RemoteEntity.Value);
|
||||
|
||||
var range = (xform.MapID != sourceXform.MapID)
|
||||
? -1
|
||||
: (sourcePos - _xforms.GetWorldPosition(xform, xformQuery)).Length();
|
||||
|
||||
if (range < 0 || range > ev.VoiceRange)
|
||||
continue;
|
||||
|
||||
ev.Recipients.TryAdd(actor.PlayerSession, new ICChatRecipientData(range, false));
|
||||
}
|
||||
}
|
||||
|
||||
public override bool SetVisionEnabled(Entity<StationAiVisionComponent> entity, bool enabled, bool announce = false)
|
||||
{
|
||||
if (!base.SetVisionEnabled(entity, enabled, announce))
|
||||
|
||||
468
Content.Server/Telephone/TelephoneSystem.cs
Normal file
468
Content.Server/Telephone/TelephoneSystem.cs
Normal file
@@ -0,0 +1,468 @@
|
||||
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;
|
||||
|
||||
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);
|
||||
|
||||
var name = Loc.GetString("speech-name-relay",
|
||||
("speaker", Name(entity)),
|
||||
("originalName", nameEv.VoiceName));
|
||||
|
||||
var volume = entity.Comp.SpeakerVolume == TelephoneVolume.Speak ? InGameICChatType.Speak : InGameICChatType.Whisper;
|
||||
_chat.TrySendInGameICMessage(entity, args.Message, volume, ChatTransmitRange.GhostRangeLimit, 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 their 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;
|
||||
|
||||
receiver.Comp.LastCallerId = GetNameAndJobOfCallingEntity(user); // 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)
|
||||
{
|
||||
HandleEndingTelephoneCalls(entity, TelephoneState.EndingCall);
|
||||
|
||||
var ev = new TelephoneCallEndedEvent();
|
||||
RaiseLocalEvent(entity, ref ev);
|
||||
}
|
||||
|
||||
public void TerminateTelephoneCalls(Entity<TelephoneComponent> entity)
|
||||
{
|
||||
HandleEndingTelephoneCalls(entity, TelephoneState.Idle);
|
||||
}
|
||||
|
||||
private void HandleEndingTelephoneCalls(Entity<TelephoneComponent> entity, TelephoneState newState)
|
||||
{
|
||||
if (entity.Comp.CurrentState == newState)
|
||||
return;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
var sourceXform = Transform(source);
|
||||
var receiverXform = Transform(receiver);
|
||||
|
||||
switch (source.Comp.TransmissionRange)
|
||||
{
|
||||
case TelephoneRange.Grid:
|
||||
return sourceXform.GridUid != null &&
|
||||
receiverXform.GridUid == sourceXform.GridUid &&
|
||||
receiver.Comp.TransmissionRange != TelephoneRange.Long;
|
||||
|
||||
case TelephoneRange.Map:
|
||||
return sourceXform.MapID == receiverXform.MapID &&
|
||||
receiver.Comp.TransmissionRange != TelephoneRange.Long;
|
||||
|
||||
case TelephoneRange.Long:
|
||||
return sourceXform.MapID != receiverXform.MapID &&
|
||||
receiver.Comp.TransmissionRange == TelephoneRange.Long;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user