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
@@ -2,6 +2,7 @@ using System.Numerics;
|
|||||||
using Content.Client.Chat.Managers;
|
using Content.Client.Chat.Managers;
|
||||||
using Content.Shared.CCVar;
|
using Content.Shared.CCVar;
|
||||||
using Content.Shared.Chat;
|
using Content.Shared.Chat;
|
||||||
|
using Content.Shared.Speech;
|
||||||
using Robust.Client.Graphics;
|
using Robust.Client.Graphics;
|
||||||
using Robust.Client.UserInterface;
|
using Robust.Client.UserInterface;
|
||||||
using Robust.Client.UserInterface.Controls;
|
using Robust.Client.UserInterface.Controls;
|
||||||
@@ -141,7 +142,12 @@ namespace Content.Client.Chat.UI
|
|||||||
Modulate = Color.White;
|
Modulate = Color.White;
|
||||||
}
|
}
|
||||||
|
|
||||||
var offset = (-_eyeManager.CurrentEye.Rotation).ToWorldVec() * -EntityVerticalOffset;
|
var baseOffset = 0f;
|
||||||
|
|
||||||
|
if (_entityManager.TryGetComponent<SpeechComponent>(_senderEntity, out var speech))
|
||||||
|
baseOffset = speech.SpeechBubbleOffset;
|
||||||
|
|
||||||
|
var offset = (-_eyeManager.CurrentEye.Rotation).ToWorldVec() * -(EntityVerticalOffset + baseOffset);
|
||||||
var worldPos = _transformSystem.GetWorldPosition(xform) + offset;
|
var worldPos = _transformSystem.GetWorldPosition(xform) + offset;
|
||||||
|
|
||||||
var lowerCenter = _eyeManager.WorldToScreen(worldPos) / UIScale;
|
var lowerCenter = _eyeManager.WorldToScreen(worldPos) / UIScale;
|
||||||
|
|||||||
101
Content.Client/Holopad/HolopadBoundUserInterface.cs
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
using Content.Shared.Holopad;
|
||||||
|
using Content.Shared.Silicons.StationAi;
|
||||||
|
using Robust.Client.Graphics;
|
||||||
|
using Robust.Client.UserInterface;
|
||||||
|
using Robust.Shared.Player;
|
||||||
|
using System.Numerics;
|
||||||
|
|
||||||
|
namespace Content.Client.Holopad;
|
||||||
|
|
||||||
|
public sealed class HolopadBoundUserInterface : BoundUserInterface
|
||||||
|
{
|
||||||
|
[Dependency] private readonly ISharedPlayerManager _playerManager = default!;
|
||||||
|
[Dependency] private readonly IClyde _displayManager = default!;
|
||||||
|
|
||||||
|
[ViewVariables]
|
||||||
|
private HolopadWindow? _window;
|
||||||
|
|
||||||
|
public HolopadBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
|
||||||
|
{
|
||||||
|
IoCManager.InjectDependencies(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Open()
|
||||||
|
{
|
||||||
|
base.Open();
|
||||||
|
|
||||||
|
_window = this.CreateWindow<HolopadWindow>();
|
||||||
|
_window.Title = Loc.GetString("holopad-window-title", ("title", EntMan.GetComponent<MetaDataComponent>(Owner).EntityName));
|
||||||
|
|
||||||
|
if (this.UiKey is not HolopadUiKey)
|
||||||
|
{
|
||||||
|
Close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var uiKey = (HolopadUiKey)this.UiKey;
|
||||||
|
|
||||||
|
// AIs will see a different holopad interface to crew when interacting with them in the world
|
||||||
|
if (uiKey == HolopadUiKey.InteractionWindow && EntMan.HasComponent<StationAiHeldComponent>(_playerManager.LocalEntity))
|
||||||
|
uiKey = HolopadUiKey.InteractionWindowForAi;
|
||||||
|
|
||||||
|
_window.SetState(Owner, uiKey);
|
||||||
|
_window.UpdateState(new Dictionary<NetEntity, string>());
|
||||||
|
|
||||||
|
// Set message actions
|
||||||
|
_window.SendHolopadStartNewCallMessageAction += SendHolopadStartNewCallMessage;
|
||||||
|
_window.SendHolopadAnswerCallMessageAction += SendHolopadAnswerCallMessage;
|
||||||
|
_window.SendHolopadEndCallMessageAction += SendHolopadEndCallMessage;
|
||||||
|
_window.SendHolopadStartBroadcastMessageAction += SendHolopadStartBroadcastMessage;
|
||||||
|
_window.SendHolopadActivateProjectorMessageAction += SendHolopadActivateProjectorMessage;
|
||||||
|
_window.SendHolopadRequestStationAiMessageAction += SendHolopadRequestStationAiMessage;
|
||||||
|
|
||||||
|
// If this call is addressed to an AI, open the window in the bottom right hand corner of the screen
|
||||||
|
if (uiKey == HolopadUiKey.AiRequestWindow)
|
||||||
|
_window.OpenCenteredAt(new Vector2(1f, 1f));
|
||||||
|
|
||||||
|
// Otherwise offset to the left so the holopad can still be seen
|
||||||
|
else
|
||||||
|
_window.OpenCenteredAt(new Vector2(0.3333f, 0.50f));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void UpdateState(BoundUserInterfaceState state)
|
||||||
|
{
|
||||||
|
base.UpdateState(state);
|
||||||
|
|
||||||
|
var castState = (HolopadBoundInterfaceState)state;
|
||||||
|
EntMan.TryGetComponent<TransformComponent>(Owner, out var xform);
|
||||||
|
|
||||||
|
_window?.UpdateState(castState.Holopads);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SendHolopadStartNewCallMessage(NetEntity receiver)
|
||||||
|
{
|
||||||
|
SendMessage(new HolopadStartNewCallMessage(receiver));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SendHolopadAnswerCallMessage()
|
||||||
|
{
|
||||||
|
SendMessage(new HolopadAnswerCallMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SendHolopadEndCallMessage()
|
||||||
|
{
|
||||||
|
SendMessage(new HolopadEndCallMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SendHolopadStartBroadcastMessage()
|
||||||
|
{
|
||||||
|
SendMessage(new HolopadStartBroadcastMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SendHolopadActivateProjectorMessage()
|
||||||
|
{
|
||||||
|
SendMessage(new HolopadActivateProjectorMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SendHolopadRequestStationAiMessage()
|
||||||
|
{
|
||||||
|
SendMessage(new HolopadStationAiRequestMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
172
Content.Client/Holopad/HolopadSystem.cs
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
using Content.Shared.Chat.TypingIndicator;
|
||||||
|
using Content.Shared.Holopad;
|
||||||
|
using Robust.Client.GameObjects;
|
||||||
|
using Robust.Client.Graphics;
|
||||||
|
using Robust.Client.Player;
|
||||||
|
using Robust.Shared.Prototypes;
|
||||||
|
using Robust.Shared.Timing;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace Content.Client.Holopad;
|
||||||
|
|
||||||
|
public sealed class HolopadSystem : SharedHolopadSystem
|
||||||
|
{
|
||||||
|
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||||
|
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||||
|
[Dependency] private readonly IGameTiming _timing = default!;
|
||||||
|
|
||||||
|
public override void Initialize()
|
||||||
|
{
|
||||||
|
base.Initialize();
|
||||||
|
|
||||||
|
SubscribeLocalEvent<HolopadHologramComponent, ComponentInit>(OnComponentInit);
|
||||||
|
SubscribeLocalEvent<HolopadHologramComponent, BeforePostShaderRenderEvent>(OnShaderRender);
|
||||||
|
SubscribeAllEvent<TypingChangedEvent>(OnTypingChanged);
|
||||||
|
|
||||||
|
SubscribeNetworkEvent<PlayerSpriteStateRequest>(OnPlayerSpriteStateRequest);
|
||||||
|
SubscribeNetworkEvent<PlayerSpriteStateMessage>(OnPlayerSpriteStateMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnComponentInit(EntityUid uid, HolopadHologramComponent component, ComponentInit ev)
|
||||||
|
{
|
||||||
|
if (!TryComp<SpriteComponent>(uid, out var sprite))
|
||||||
|
return;
|
||||||
|
|
||||||
|
UpdateHologramSprite(uid);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnShaderRender(EntityUid uid, HolopadHologramComponent component, BeforePostShaderRenderEvent ev)
|
||||||
|
{
|
||||||
|
if (ev.Sprite.PostShader == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
ev.Sprite.PostShader.SetParameter("t", (float)_timing.CurTime.TotalSeconds * component.ScrollRate);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnTypingChanged(TypingChangedEvent ev, EntitySessionEventArgs args)
|
||||||
|
{
|
||||||
|
var uid = args.SenderSession.AttachedEntity;
|
||||||
|
|
||||||
|
if (!Exists(uid))
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!HasComp<HolopadUserComponent>(uid))
|
||||||
|
return;
|
||||||
|
|
||||||
|
var netEv = new HolopadUserTypingChangedEvent(GetNetEntity(uid.Value), ev.IsTyping);
|
||||||
|
RaiseNetworkEvent(netEv);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnPlayerSpriteStateRequest(PlayerSpriteStateRequest ev)
|
||||||
|
{
|
||||||
|
var targetPlayer = GetEntity(ev.TargetPlayer);
|
||||||
|
var player = _playerManager.LocalSession?.AttachedEntity;
|
||||||
|
|
||||||
|
// Ignore the request if received by a player who isn't the target
|
||||||
|
if (targetPlayer != player)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!TryComp<SpriteComponent>(player, out var playerSprite))
|
||||||
|
return;
|
||||||
|
|
||||||
|
var spriteLayerData = new List<PrototypeLayerData>();
|
||||||
|
|
||||||
|
if (playerSprite.Visible)
|
||||||
|
{
|
||||||
|
// Record the RSI paths, state names and shader paramaters of all visible layers
|
||||||
|
for (int i = 0; i < playerSprite.AllLayers.Count(); i++)
|
||||||
|
{
|
||||||
|
if (!playerSprite.TryGetLayer(i, out var layer))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (!layer.Visible ||
|
||||||
|
string.IsNullOrEmpty(layer.ActualRsi?.Path.ToString()) ||
|
||||||
|
string.IsNullOrEmpty(layer.State.Name))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var layerDatum = new PrototypeLayerData();
|
||||||
|
layerDatum.RsiPath = layer.ActualRsi.Path.ToString();
|
||||||
|
layerDatum.State = layer.State.Name;
|
||||||
|
|
||||||
|
if (layer.CopyToShaderParameters != null)
|
||||||
|
{
|
||||||
|
var key = (string)layer.CopyToShaderParameters.LayerKey;
|
||||||
|
|
||||||
|
if (playerSprite.LayerMapTryGet(key, out var otherLayerIdx) &&
|
||||||
|
playerSprite.TryGetLayer(otherLayerIdx, out var otherLayer) &&
|
||||||
|
otherLayer.Visible)
|
||||||
|
{
|
||||||
|
layerDatum.MapKeys = new() { key };
|
||||||
|
|
||||||
|
layerDatum.CopyToShaderParameters = new PrototypeCopyToShaderParameters()
|
||||||
|
{
|
||||||
|
LayerKey = key,
|
||||||
|
ParameterTexture = layer.CopyToShaderParameters.ParameterTexture,
|
||||||
|
ParameterUV = layer.CopyToShaderParameters.ParameterUV
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
spriteLayerData.Add(layerDatum);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the recorded data to the server
|
||||||
|
var evResponse = new PlayerSpriteStateMessage(ev.TargetPlayer, spriteLayerData.ToArray());
|
||||||
|
RaiseNetworkEvent(evResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnPlayerSpriteStateMessage(PlayerSpriteStateMessage ev)
|
||||||
|
{
|
||||||
|
UpdateHologramSprite(GetEntity(ev.SpriteEntity), ev.SpriteLayerData);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateHologramSprite(EntityUid uid, PrototypeLayerData[]? layerData = null)
|
||||||
|
{
|
||||||
|
if (!TryComp<SpriteComponent>(uid, out var hologramSprite))
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!TryComp<HolopadHologramComponent>(uid, out var holopadhologram))
|
||||||
|
return;
|
||||||
|
|
||||||
|
for (int i = hologramSprite.AllLayers.Count() - 1; i >= 0; i--)
|
||||||
|
hologramSprite.RemoveLayer(i);
|
||||||
|
|
||||||
|
if (layerData == null || layerData.Length == 0)
|
||||||
|
{
|
||||||
|
layerData = new PrototypeLayerData[1];
|
||||||
|
layerData[0] = new PrototypeLayerData()
|
||||||
|
{
|
||||||
|
RsiPath = holopadhologram.RsiPath,
|
||||||
|
State = holopadhologram.RsiState
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < layerData.Length; i++)
|
||||||
|
{
|
||||||
|
var layer = layerData[i];
|
||||||
|
layer.Shader = "unshaded";
|
||||||
|
|
||||||
|
hologramSprite.AddLayer(layerData[i], i);
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateHologramShader(uid, hologramSprite, holopadhologram);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateHologramShader(EntityUid uid, SpriteComponent sprite, HolopadHologramComponent holopadHologram)
|
||||||
|
{
|
||||||
|
// Find the texture height of the largest layer
|
||||||
|
float texHeight = sprite.AllLayers.Max(x => x.PixelSize.Y);
|
||||||
|
|
||||||
|
var instance = _prototypeManager.Index<ShaderPrototype>(holopadHologram.ShaderName).InstanceUnique();
|
||||||
|
instance.SetParameter("color1", new Vector3(holopadHologram.Color1.R, holopadHologram.Color1.G, holopadHologram.Color1.B));
|
||||||
|
instance.SetParameter("color2", new Vector3(holopadHologram.Color2.R, holopadHologram.Color2.G, holopadHologram.Color2.B));
|
||||||
|
instance.SetParameter("alpha", holopadHologram.Alpha);
|
||||||
|
instance.SetParameter("intensity", holopadHologram.Intensity);
|
||||||
|
instance.SetParameter("texHeight", texHeight);
|
||||||
|
instance.SetParameter("t", (float)_timing.CurTime.TotalSeconds * holopadHologram.ScrollRate);
|
||||||
|
|
||||||
|
sprite.PostShader = instance;
|
||||||
|
sprite.RaiseShaderEvent = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
107
Content.Client/Holopad/HolopadWindow.xaml
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
<controls:FancyWindow xmlns="https://spacestation14.io"
|
||||||
|
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
|
||||||
|
xmlns:gfx="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
|
||||||
|
Resizable="False"
|
||||||
|
MaxSize="400 800"
|
||||||
|
MinSize="400 150">
|
||||||
|
<BoxContainer Orientation="Vertical" VerticalExpand="True" HorizontalExpand="True">
|
||||||
|
|
||||||
|
<BoxContainer Name="ControlsLockOutContainer" Orientation="Vertical" VerticalExpand="True" HorizontalExpand="True" ReservesSpace="False" Visible="False">
|
||||||
|
<!-- Header text -->
|
||||||
|
<controls:StripeBack>
|
||||||
|
<PanelContainer>
|
||||||
|
<RichTextLabel Name="EmergencyBroadcastText" VerticalAlignment="Center" HorizontalAlignment="Center" Margin="10 10 10 10" ReservesSpace="False"/>
|
||||||
|
</PanelContainer>
|
||||||
|
</controls:StripeBack>
|
||||||
|
|
||||||
|
<Label Text="{Loc 'holopad-window-controls-locked-out'}" HorizontalAlignment="Center" Margin="10 5 10 0" ReservesSpace="False"/>
|
||||||
|
<RichTextLabel Name="LockOutIdText" HorizontalAlignment="Center" Margin="10 5 10 0" ReservesSpace="False"/>
|
||||||
|
<Label Name="LockOutCountDownText" Text="{Loc 'holopad-window-controls-unlock-countdown'}" HorizontalAlignment="Center" Margin="10 15 10 10" ReservesSpace="False"/>
|
||||||
|
</BoxContainer>
|
||||||
|
|
||||||
|
<BoxContainer Name="ControlsContainer" Orientation="Vertical" VerticalExpand="True" HorizontalExpand="True" ReservesSpace="False">
|
||||||
|
|
||||||
|
<!-- Active call controls (either this or the call placement controls will be active) -->
|
||||||
|
<BoxContainer Name="ActiveCallControlsContainer" Orientation="Vertical" VerticalExpand="True" HorizontalExpand="True" ReservesSpace="False">
|
||||||
|
|
||||||
|
<!-- Header text -->
|
||||||
|
<BoxContainer MinHeight="60" Orientation="Vertical" VerticalAlignment="Center">
|
||||||
|
<Label Name="CallStatusText" Margin="10 5 10 0" ReservesSpace="False"/>
|
||||||
|
<RichTextLabel Name="CallerIdText" HorizontalAlignment="Center" Margin="0 0 0 0" ReservesSpace="False"/>
|
||||||
|
</BoxContainer>
|
||||||
|
|
||||||
|
<!-- Controls (the answer call button is absent when the phone is not ringing) -->
|
||||||
|
<GridContainer Columns="2" ReservesSpace="False">
|
||||||
|
<Control HorizontalExpand="True" Margin="10 0 2 5">
|
||||||
|
<Button Name="AnswerCallButton" Text="{Loc 'holopad-window-answer-call'}" StyleClasses="OpenRight" Margin="0 0 0 5" Disabled="True"/>
|
||||||
|
</Control>
|
||||||
|
<Control HorizontalExpand="True" Margin="2 0 10 5">
|
||||||
|
<Button Name="EndCallButton" Text="{Loc 'holopad-window-end-call'}" StyleClasses="OpenLeft" Margin="0 0 0 5" Disabled="True"/>
|
||||||
|
</Control>
|
||||||
|
</GridContainer>
|
||||||
|
|
||||||
|
</BoxContainer>
|
||||||
|
|
||||||
|
<!-- Call placement controls (either this or the active call controls will be active) -->
|
||||||
|
<BoxContainer Name="CallPlacementControlsContainer" Orientation="Vertical" VerticalExpand="True" HorizontalExpand="True" ReservesSpace="False">
|
||||||
|
|
||||||
|
<controls:StripeBack>
|
||||||
|
<PanelContainer>
|
||||||
|
<BoxContainer Orientation="Vertical">
|
||||||
|
<RichTextLabel Name="SubtitleText" HorizontalAlignment="Center" Margin="0 5 0 0"/>
|
||||||
|
<RichTextLabel Name="OptionsText" HorizontalAlignment="Center" Margin="0 0 0 5"/>
|
||||||
|
</BoxContainer>
|
||||||
|
</PanelContainer>
|
||||||
|
</controls:StripeBack>
|
||||||
|
|
||||||
|
<!-- Request the station AI or activate the holopad projector (only one of these should be active at a time) -->
|
||||||
|
<BoxContainer Name="RequestStationAiContainer" Orientation="Vertical" ReservesSpace="False" Visible="False">
|
||||||
|
<Button Name="RequestStationAiButton" Text="{Loc 'holopad-window-request-station-ai'}" Margin="10 5 10 5" Disabled="False"/>
|
||||||
|
</BoxContainer>
|
||||||
|
|
||||||
|
<BoxContainer Name="ActivateProjectorContainer" Orientation="Vertical" ReservesSpace="False" Visible="False">
|
||||||
|
<Button Name="ActivateProjectorButton" Text="{Loc 'holopad-window-activate-projector'}" Margin="10 5 10 5" Disabled="False"/>
|
||||||
|
</BoxContainer>
|
||||||
|
|
||||||
|
<!-- List of contactable holopads (the list is created in C#) -->
|
||||||
|
<BoxContainer Name="HolopadContactListContainer" Orientation="Vertical" Margin="10 0 10 5" ReservesSpace="False" Visible="False">
|
||||||
|
<PanelContainer Name="HolopadContactListHeaderPanel">
|
||||||
|
<Label Text="{Loc 'holopad-window-select-contact-from-list'}" HorizontalAlignment="Center" Margin="0 3 0 3"/>
|
||||||
|
</PanelContainer>
|
||||||
|
|
||||||
|
<PanelContainer Name="HolopadContactListPanel">
|
||||||
|
<ScrollContainer HorizontalExpand="True" VerticalExpand="True" Margin="8, 8, 8, 8" MinHeight="256">
|
||||||
|
|
||||||
|
<!-- If there is no data yet, this will be displayed -->
|
||||||
|
<BoxContainer Name="FetchingAvailableHolopadsContainer" HorizontalAlignment="Center" HorizontalExpand="True" VerticalExpand="True" ReservesSpace="False">
|
||||||
|
<Label Text="{Loc 'holopad-window-fetching-contacts-list'}" HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||||
|
</BoxContainer>
|
||||||
|
|
||||||
|
<!-- Container for the contacts -->
|
||||||
|
<BoxContainer Name="ContactsList" Orientation="Vertical" HorizontalExpand="True" VerticalExpand="True" Margin="10 0 10 0"/>
|
||||||
|
</ScrollContainer>
|
||||||
|
</PanelContainer>
|
||||||
|
</BoxContainer>
|
||||||
|
|
||||||
|
<!-- Button to start an emergency broadcast (the user requires a certain level of access to interact with it) -->
|
||||||
|
<BoxContainer Name="StartBroadcastContainer" Orientation="Vertical" ReservesSpace="False" Visible="False">
|
||||||
|
<Button Name="StartBroadcastButton" Text="{Loc 'holopad-window-emergency-broadcast'}" Margin="10 0 10 5" Disabled="False" ReservesSpace="False"/>
|
||||||
|
</BoxContainer>
|
||||||
|
|
||||||
|
</BoxContainer>
|
||||||
|
</BoxContainer>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<BoxContainer Orientation="Vertical">
|
||||||
|
<PanelContainer StyleClasses="LowDivider" />
|
||||||
|
<BoxContainer Orientation="Horizontal" Margin="10 2 5 0" VerticalAlignment="Bottom">
|
||||||
|
<Label Text="{Loc 'holopad-window-flavor-left'}" StyleClasses="WindowFooterText" />
|
||||||
|
<Label Text="{Loc 'holopad-window-flavor-right'}" StyleClasses="WindowFooterText"
|
||||||
|
HorizontalAlignment="Right" HorizontalExpand="True" Margin="0 0 5 0" />
|
||||||
|
<TextureRect StyleClasses="NTLogoDark" Stretch="KeepAspectCentered"
|
||||||
|
VerticalAlignment="Center" HorizontalAlignment="Right" SetSize="19 19"/>
|
||||||
|
</BoxContainer>
|
||||||
|
</BoxContainer>
|
||||||
|
</BoxContainer>
|
||||||
|
|
||||||
|
</controls:FancyWindow>
|
||||||
338
Content.Client/Holopad/HolopadWindow.xaml.cs
Normal file
@@ -0,0 +1,338 @@
|
|||||||
|
using Content.Client.Popups;
|
||||||
|
using Content.Client.UserInterface.Controls;
|
||||||
|
using Content.Shared.Access.Systems;
|
||||||
|
using Content.Shared.Holopad;
|
||||||
|
using Content.Shared.Telephone;
|
||||||
|
using Robust.Client.AutoGenerated;
|
||||||
|
using Robust.Client.Graphics;
|
||||||
|
using Robust.Client.Player;
|
||||||
|
using Robust.Client.UserInterface.Controls;
|
||||||
|
using Robust.Client.UserInterface.XAML;
|
||||||
|
using Robust.Shared.Timing;
|
||||||
|
using Robust.Shared.Utility;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace Content.Client.Holopad;
|
||||||
|
|
||||||
|
[GenerateTypedNameReferences]
|
||||||
|
public sealed partial class HolopadWindow : FancyWindow
|
||||||
|
{
|
||||||
|
[Dependency] private readonly IEntityManager _entManager = default!;
|
||||||
|
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||||
|
[Dependency] private readonly IGameTiming _timing = default!;
|
||||||
|
|
||||||
|
private readonly SharedHolopadSystem _holopadSystem = default!;
|
||||||
|
private readonly SharedTelephoneSystem _telephoneSystem = default!;
|
||||||
|
private readonly AccessReaderSystem _accessReaderSystem = default!;
|
||||||
|
private readonly PopupSystem _popupSystem = default!;
|
||||||
|
|
||||||
|
private EntityUid? _owner = null;
|
||||||
|
private HolopadUiKey _currentUiKey;
|
||||||
|
private TelephoneState _currentState;
|
||||||
|
private TelephoneState _previousState;
|
||||||
|
private TimeSpan _buttonUnlockTime;
|
||||||
|
private float _updateTimer = 0.25f;
|
||||||
|
|
||||||
|
private const float UpdateTime = 0.25f;
|
||||||
|
private TimeSpan _buttonUnlockDelay = TimeSpan.FromSeconds(0.5f);
|
||||||
|
|
||||||
|
public event Action<NetEntity>? SendHolopadStartNewCallMessageAction;
|
||||||
|
public event Action? SendHolopadAnswerCallMessageAction;
|
||||||
|
public event Action? SendHolopadEndCallMessageAction;
|
||||||
|
public event Action? SendHolopadStartBroadcastMessageAction;
|
||||||
|
public event Action? SendHolopadActivateProjectorMessageAction;
|
||||||
|
public event Action? SendHolopadRequestStationAiMessageAction;
|
||||||
|
|
||||||
|
public HolopadWindow()
|
||||||
|
{
|
||||||
|
RobustXamlLoader.Load(this);
|
||||||
|
IoCManager.InjectDependencies(this);
|
||||||
|
|
||||||
|
_holopadSystem = _entManager.System<SharedHolopadSystem>();
|
||||||
|
_telephoneSystem = _entManager.System<SharedTelephoneSystem>();
|
||||||
|
_accessReaderSystem = _entManager.System<AccessReaderSystem>();
|
||||||
|
_popupSystem = _entManager.System<PopupSystem>();
|
||||||
|
|
||||||
|
_buttonUnlockTime = _timing.CurTime + _buttonUnlockDelay;
|
||||||
|
|
||||||
|
// Assign button actions
|
||||||
|
AnswerCallButton.OnPressed += args => { OnHolopadAnswerCallMessage(); };
|
||||||
|
EndCallButton.OnPressed += args => { OnHolopadEndCallMessage(); };
|
||||||
|
StartBroadcastButton.OnPressed += args => { OnHolopadStartBroadcastMessage(); };
|
||||||
|
ActivateProjectorButton.OnPressed += args => { OnHolopadActivateProjectorMessage(); };
|
||||||
|
RequestStationAiButton.OnPressed += args => { OnHolopadRequestStationAiMessage(); };
|
||||||
|
|
||||||
|
// XML formatting
|
||||||
|
AnswerCallButton.AddStyleClass("ButtonAccept");
|
||||||
|
EndCallButton.AddStyleClass("Caution");
|
||||||
|
StartBroadcastButton.AddStyleClass("Caution");
|
||||||
|
|
||||||
|
HolopadContactListPanel.PanelOverride = new StyleBoxFlat
|
||||||
|
{
|
||||||
|
BackgroundColor = new Color(47, 47, 59) * Color.DarkGray,
|
||||||
|
BorderColor = new Color(82, 82, 82), //new Color(70, 73, 102),
|
||||||
|
BorderThickness = new Thickness(2),
|
||||||
|
};
|
||||||
|
|
||||||
|
HolopadContactListHeaderPanel.PanelOverride = new StyleBoxFlat
|
||||||
|
{
|
||||||
|
BackgroundColor = new Color(82, 82, 82),
|
||||||
|
};
|
||||||
|
|
||||||
|
EmergencyBroadcastText.SetMessage(FormattedMessage.FromMarkupOrThrow(Loc.GetString("holopad-window-emergency-broadcast-in-progress")));
|
||||||
|
SubtitleText.SetMessage(FormattedMessage.FromMarkupOrThrow(Loc.GetString("holopad-window-subtitle")));
|
||||||
|
OptionsText.SetMessage(FormattedMessage.FromMarkupOrThrow(Loc.GetString("holopad-window-options")));
|
||||||
|
}
|
||||||
|
|
||||||
|
#region: Button actions
|
||||||
|
|
||||||
|
private void OnSendHolopadStartNewCallMessage(NetEntity receiver)
|
||||||
|
{
|
||||||
|
SendHolopadStartNewCallMessageAction?.Invoke(receiver);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnHolopadAnswerCallMessage()
|
||||||
|
{
|
||||||
|
SendHolopadAnswerCallMessageAction?.Invoke();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnHolopadEndCallMessage()
|
||||||
|
{
|
||||||
|
SendHolopadEndCallMessageAction?.Invoke();
|
||||||
|
|
||||||
|
if (_currentUiKey == HolopadUiKey.AiRequestWindow)
|
||||||
|
Close();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnHolopadStartBroadcastMessage()
|
||||||
|
{
|
||||||
|
if (_playerManager.LocalSession?.AttachedEntity == null || _owner == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var player = _playerManager.LocalSession.AttachedEntity;
|
||||||
|
|
||||||
|
if (!_accessReaderSystem.IsAllowed(player.Value, _owner.Value))
|
||||||
|
{
|
||||||
|
_popupSystem.PopupClient(Loc.GetString("holopad-window-access-denied"), _owner.Value, player.Value);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SendHolopadStartBroadcastMessageAction?.Invoke();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnHolopadActivateProjectorMessage()
|
||||||
|
{
|
||||||
|
SendHolopadActivateProjectorMessageAction?.Invoke();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnHolopadRequestStationAiMessage()
|
||||||
|
{
|
||||||
|
SendHolopadRequestStationAiMessageAction?.Invoke();
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
public void SetState(EntityUid owner, HolopadUiKey uiKey)
|
||||||
|
{
|
||||||
|
_owner = owner;
|
||||||
|
_currentUiKey = uiKey;
|
||||||
|
|
||||||
|
// Determines what UI containers are available to the user.
|
||||||
|
// Components of these will be toggled on and off when
|
||||||
|
// UpdateAppearance() is called
|
||||||
|
|
||||||
|
switch (uiKey)
|
||||||
|
{
|
||||||
|
case HolopadUiKey.InteractionWindow:
|
||||||
|
RequestStationAiContainer.Visible = true;
|
||||||
|
HolopadContactListContainer.Visible = true;
|
||||||
|
StartBroadcastContainer.Visible = true;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case HolopadUiKey.InteractionWindowForAi:
|
||||||
|
ActivateProjectorContainer.Visible = true;
|
||||||
|
StartBroadcastContainer.Visible = true;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case HolopadUiKey.AiActionWindow:
|
||||||
|
HolopadContactListContainer.Visible = true;
|
||||||
|
StartBroadcastContainer.Visible = true;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case HolopadUiKey.AiRequestWindow:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UpdateState(Dictionary<NetEntity, string> holopads)
|
||||||
|
{
|
||||||
|
if (_owner == null || !_entManager.TryGetComponent<TelephoneComponent>(_owner.Value, out var telephone))
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Caller ID text
|
||||||
|
var callerId = _telephoneSystem.GetFormattedCallerIdForEntity(telephone.LastCallerId.Item1, telephone.LastCallerId.Item2, Color.LightGray, "Default", 11);
|
||||||
|
|
||||||
|
CallerIdText.SetMessage(FormattedMessage.FromMarkupOrThrow(callerId));
|
||||||
|
LockOutIdText.SetMessage(FormattedMessage.FromMarkupOrThrow(callerId));
|
||||||
|
|
||||||
|
// Sort holopads alphabetically
|
||||||
|
var holopadArray = holopads.ToArray();
|
||||||
|
Array.Sort(holopadArray, AlphabeticalSort);
|
||||||
|
|
||||||
|
// Clear excess children from the contact list
|
||||||
|
while (ContactsList.ChildCount > holopadArray.Length)
|
||||||
|
ContactsList.RemoveChild(ContactsList.GetChild(ContactsList.ChildCount - 1));
|
||||||
|
|
||||||
|
// Make / update required children
|
||||||
|
for (int i = 0; i < holopadArray.Length; i++)
|
||||||
|
{
|
||||||
|
var (netEntity, label) = holopadArray[i];
|
||||||
|
|
||||||
|
if (i >= ContactsList.ChildCount)
|
||||||
|
{
|
||||||
|
var newContactButton = new HolopadContactButton();
|
||||||
|
newContactButton.OnPressed += args => { OnSendHolopadStartNewCallMessage(newContactButton.NetEntity); };
|
||||||
|
|
||||||
|
ContactsList.AddChild(newContactButton);
|
||||||
|
}
|
||||||
|
|
||||||
|
var child = ContactsList.GetChild(i);
|
||||||
|
|
||||||
|
if (child is not HolopadContactButton)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var contactButton = (HolopadContactButton)child;
|
||||||
|
contactButton.UpdateValues(netEntity, label);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update buttons
|
||||||
|
UpdateAppearance();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateAppearance()
|
||||||
|
{
|
||||||
|
if (_owner == null || !_entManager.TryGetComponent<TelephoneComponent>(_owner.Value, out var telephone))
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (_owner == null || !_entManager.TryGetComponent<HolopadComponent>(_owner.Value, out var holopad))
|
||||||
|
return;
|
||||||
|
|
||||||
|
var hasBroadcastAccess = !_holopadSystem.IsHolopadBroadcastOnCoolDown((_owner.Value, holopad));
|
||||||
|
var localPlayer = _playerManager.LocalSession?.AttachedEntity;
|
||||||
|
|
||||||
|
ControlsLockOutContainer.Visible = _holopadSystem.IsHolopadControlLocked((_owner.Value, holopad), localPlayer);
|
||||||
|
ControlsContainer.Visible = !ControlsLockOutContainer.Visible;
|
||||||
|
|
||||||
|
// Temporarily disable the interface buttons when the call state changes to prevent any misclicks
|
||||||
|
if (_currentState != telephone.CurrentState)
|
||||||
|
{
|
||||||
|
_previousState = _currentState;
|
||||||
|
_currentState = telephone.CurrentState;
|
||||||
|
_buttonUnlockTime = _timing.CurTime + _buttonUnlockDelay;
|
||||||
|
}
|
||||||
|
|
||||||
|
var lockButtons = _timing.CurTime < _buttonUnlockTime;
|
||||||
|
|
||||||
|
// Make / update required children
|
||||||
|
foreach (var child in ContactsList.Children)
|
||||||
|
{
|
||||||
|
if (child is not HolopadContactButton)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var contactButton = (HolopadContactButton)child;
|
||||||
|
contactButton.Disabled = (_currentState != TelephoneState.Idle || lockButtons);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update control text
|
||||||
|
var cooldown = _holopadSystem.GetHolopadBroadcastCoolDown((_owner.Value, holopad));
|
||||||
|
var cooldownString = $"{cooldown.Minutes:00}:{cooldown.Seconds:00}";
|
||||||
|
|
||||||
|
StartBroadcastButton.Text = _holopadSystem.IsHolopadBroadcastOnCoolDown((_owner.Value, holopad)) ?
|
||||||
|
Loc.GetString("holopad-window-emergency-broadcast-with-countdown", ("countdown", cooldownString)) :
|
||||||
|
Loc.GetString("holopad-window-emergency-broadcast");
|
||||||
|
|
||||||
|
var lockout = _holopadSystem.GetHolopadControlLockedPeriod((_owner.Value, holopad));
|
||||||
|
var lockoutString = $"{lockout.Minutes:00}:{lockout.Seconds:00}";
|
||||||
|
|
||||||
|
LockOutCountDownText.Text = Loc.GetString("holopad-window-controls-unlock-countdown", ("countdown", lockoutString));
|
||||||
|
|
||||||
|
switch (_currentState)
|
||||||
|
{
|
||||||
|
case TelephoneState.Idle:
|
||||||
|
CallStatusText.Text = Loc.GetString("holopad-window-no-calls-in-progress"); break;
|
||||||
|
|
||||||
|
case TelephoneState.Calling:
|
||||||
|
CallStatusText.Text = Loc.GetString("holopad-window-outgoing-call"); break;
|
||||||
|
|
||||||
|
case TelephoneState.Ringing:
|
||||||
|
CallStatusText.Text = (_currentUiKey == HolopadUiKey.AiRequestWindow) ?
|
||||||
|
Loc.GetString("holopad-window-ai-request") : Loc.GetString("holopad-window-incoming-call"); break;
|
||||||
|
|
||||||
|
case TelephoneState.InCall:
|
||||||
|
CallStatusText.Text = Loc.GetString("holopad-window-call-in-progress"); break;
|
||||||
|
|
||||||
|
case TelephoneState.EndingCall:
|
||||||
|
if (_previousState == TelephoneState.Calling || _previousState == TelephoneState.Idle)
|
||||||
|
CallStatusText.Text = Loc.GetString("holopad-window-call-rejected");
|
||||||
|
else
|
||||||
|
CallStatusText.Text = Loc.GetString("holopad-window-call-ending");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update control disability
|
||||||
|
AnswerCallButton.Disabled = (_currentState != TelephoneState.Ringing || lockButtons);
|
||||||
|
EndCallButton.Disabled = (_currentState == TelephoneState.Idle || _currentState == TelephoneState.EndingCall || lockButtons);
|
||||||
|
StartBroadcastButton.Disabled = (_currentState != TelephoneState.Idle || !hasBroadcastAccess || lockButtons);
|
||||||
|
RequestStationAiButton.Disabled = (_currentState != TelephoneState.Idle || lockButtons);
|
||||||
|
ActivateProjectorButton.Disabled = (_currentState != TelephoneState.Idle || lockButtons);
|
||||||
|
|
||||||
|
// Update control visibility
|
||||||
|
FetchingAvailableHolopadsContainer.Visible = (ContactsList.ChildCount == 0);
|
||||||
|
ActiveCallControlsContainer.Visible = (_currentState != TelephoneState.Idle || _currentUiKey == HolopadUiKey.AiRequestWindow);
|
||||||
|
CallPlacementControlsContainer.Visible = !ActiveCallControlsContainer.Visible;
|
||||||
|
CallerIdText.Visible = (_currentState == TelephoneState.Ringing);
|
||||||
|
AnswerCallButton.Visible = (_currentState == TelephoneState.Ringing);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void FrameUpdate(FrameEventArgs args)
|
||||||
|
{
|
||||||
|
base.FrameUpdate(args);
|
||||||
|
|
||||||
|
_updateTimer += args.DeltaSeconds;
|
||||||
|
|
||||||
|
if (_updateTimer >= UpdateTime)
|
||||||
|
{
|
||||||
|
_updateTimer -= UpdateTime;
|
||||||
|
UpdateAppearance();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class HolopadContactButton : Button
|
||||||
|
{
|
||||||
|
public NetEntity NetEntity;
|
||||||
|
|
||||||
|
public HolopadContactButton()
|
||||||
|
{
|
||||||
|
HorizontalExpand = true;
|
||||||
|
SetHeight = 32;
|
||||||
|
Margin = new Thickness(0f, 1f, 0f, 1f);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UpdateValues(NetEntity netEntity, string label)
|
||||||
|
{
|
||||||
|
NetEntity = netEntity;
|
||||||
|
Text = Loc.GetString("holopad-window-contact-label", ("label", label));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int AlphabeticalSort(KeyValuePair<NetEntity, string> x, KeyValuePair<NetEntity, string> y)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(x.Value))
|
||||||
|
return -1;
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(y.Value))
|
||||||
|
return 1;
|
||||||
|
|
||||||
|
return x.Value.CompareTo(y.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -110,6 +110,7 @@ namespace Content.Client.Stylesheets
|
|||||||
|
|
||||||
public static readonly Color ButtonColorGoodDefault = Color.FromHex("#3E6C45");
|
public static readonly Color ButtonColorGoodDefault = Color.FromHex("#3E6C45");
|
||||||
public static readonly Color ButtonColorGoodHovered = Color.FromHex("#31843E");
|
public static readonly Color ButtonColorGoodHovered = Color.FromHex("#31843E");
|
||||||
|
public static readonly Color ButtonColorGoodDisabled = Color.FromHex("#164420");
|
||||||
|
|
||||||
//NavMap
|
//NavMap
|
||||||
public static readonly Color PointRed = Color.FromHex("#B02E26");
|
public static readonly Color PointRed = Color.FromHex("#B02E26");
|
||||||
@@ -1499,6 +1500,20 @@ namespace Content.Client.Stylesheets
|
|||||||
|
|
||||||
Element<Button>().Class("ButtonColorGreen").Pseudo(ContainerButton.StylePseudoClassHover)
|
Element<Button>().Class("ButtonColorGreen").Pseudo(ContainerButton.StylePseudoClassHover)
|
||||||
.Prop(Control.StylePropertyModulateSelf, ButtonColorGoodHovered),
|
.Prop(Control.StylePropertyModulateSelf, ButtonColorGoodHovered),
|
||||||
|
|
||||||
|
// Accept button (merge with green button?) ---
|
||||||
|
Element<Button>().Class("ButtonAccept")
|
||||||
|
.Prop(Control.StylePropertyModulateSelf, ButtonColorGoodDefault),
|
||||||
|
|
||||||
|
Element<Button>().Class("ButtonAccept").Pseudo(ContainerButton.StylePseudoClassNormal)
|
||||||
|
.Prop(Control.StylePropertyModulateSelf, ButtonColorGoodDefault),
|
||||||
|
|
||||||
|
Element<Button>().Class("ButtonAccept").Pseudo(ContainerButton.StylePseudoClassHover)
|
||||||
|
.Prop(Control.StylePropertyModulateSelf, ButtonColorGoodHovered),
|
||||||
|
|
||||||
|
Element<Button>().Class("ButtonAccept").Pseudo(ContainerButton.StylePseudoClassDisabled)
|
||||||
|
.Prop(Control.StylePropertyModulateSelf, ButtonColorGoodDisabled),
|
||||||
|
|
||||||
// ---
|
// ---
|
||||||
|
|
||||||
// Small Button ---
|
// Small Button ---
|
||||||
|
|||||||
8
Content.Client/Telephone/TelephoneSystem.cs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
using Content.Shared.Telephone;
|
||||||
|
|
||||||
|
namespace Content.Client.Telephone;
|
||||||
|
|
||||||
|
public sealed class TelephoneSystem : SharedTelephoneSystem
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
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 sealed partial class AiVisionWireAction : ComponentWireAction<StationAiVisionComponent>
|
||||||
{
|
{
|
||||||
public override string Name { get; set; } = "wire-name-ai-vision-light";
|
public override string Name { get; set; } = "wire-name-ai-vision-light";
|
||||||
public override Color Color { get; set; } = Color.DeepSkyBlue;
|
public override Color Color { get; set; } = Color.White;
|
||||||
public override object StatusKey => AirlockWireStatus.AiControlIndicator;
|
public override object StatusKey => AirlockWireStatus.AiVisionIndicator;
|
||||||
|
|
||||||
public override StatusLightState? GetLightState(Wire wire, StationAiVisionComponent component)
|
public override StatusLightState? GetLightState(Wire wire, StationAiVisionComponent component)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Content.Server.Chat.Managers;
|
using Content.Server.Chat.Managers;
|
||||||
|
using Content.Server.Chat.Systems;
|
||||||
using Content.Shared.Chat;
|
using Content.Shared.Chat;
|
||||||
using Content.Shared.Mind;
|
using Content.Shared.Mind;
|
||||||
using Content.Shared.Roles;
|
using Content.Shared.Roles;
|
||||||
@@ -8,6 +9,7 @@ using Content.Shared.StationAi;
|
|||||||
using Robust.Shared.Audio;
|
using Robust.Shared.Audio;
|
||||||
using Robust.Shared.Map.Components;
|
using Robust.Shared.Map.Components;
|
||||||
using Robust.Shared.Player;
|
using Robust.Shared.Player;
|
||||||
|
using static Content.Server.Chat.Systems.ChatSystem;
|
||||||
|
|
||||||
namespace Content.Server.Silicons.StationAi;
|
namespace Content.Server.Silicons.StationAi;
|
||||||
|
|
||||||
@@ -15,11 +17,50 @@ public sealed class StationAiSystem : SharedStationAiSystem
|
|||||||
{
|
{
|
||||||
[Dependency] private readonly IChatManager _chats = default!;
|
[Dependency] private readonly IChatManager _chats = default!;
|
||||||
[Dependency] private readonly EntityLookupSystem _lookup = default!;
|
[Dependency] private readonly EntityLookupSystem _lookup = default!;
|
||||||
|
[Dependency] private readonly SharedTransformSystem _xforms = default!;
|
||||||
[Dependency] private readonly SharedMindSystem _mind = default!;
|
[Dependency] private readonly SharedMindSystem _mind = default!;
|
||||||
[Dependency] private readonly SharedRoleSystem _roles = default!;
|
[Dependency] private readonly SharedRoleSystem _roles = default!;
|
||||||
|
|
||||||
private readonly HashSet<Entity<StationAiCoreComponent>> _ais = new();
|
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)
|
public override bool SetVisionEnabled(Entity<StationAiVisionComponent> entity, bool enabled, bool announce = false)
|
||||||
{
|
{
|
||||||
if (!base.SetVisionEnabled(entity, enabled, announce))
|
if (!base.SetVisionEnabled(entity, enabled, announce))
|
||||||
|
|||||||
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
using Robust.Shared.Serialization;
|
using Robust.Shared.Serialization;
|
||||||
|
|
||||||
namespace Content.Shared.Doors
|
namespace Content.Shared.Doors
|
||||||
{
|
{
|
||||||
@@ -9,6 +9,7 @@ namespace Content.Shared.Doors
|
|||||||
BoltIndicator,
|
BoltIndicator,
|
||||||
BoltLightIndicator,
|
BoltLightIndicator,
|
||||||
AiControlIndicator,
|
AiControlIndicator,
|
||||||
|
AiVisionIndicator,
|
||||||
TimingIndicator,
|
TimingIndicator,
|
||||||
SafetyIndicator,
|
SafetyIndicator,
|
||||||
}
|
}
|
||||||
|
|||||||
13
Content.Shared/Holopad/HolographicAvatarComponent.cs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
using Robust.Shared.GameStates;
|
||||||
|
|
||||||
|
namespace Content.Shared.Holopad;
|
||||||
|
|
||||||
|
[RegisterComponent, NetworkedComponent]
|
||||||
|
public sealed partial class HolographicAvatarComponent : Component
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The prototype sprite layer data for the hologram
|
||||||
|
/// </summary>
|
||||||
|
[DataField]
|
||||||
|
public PrototypeLayerData[] LayerData;
|
||||||
|
}
|
||||||
133
Content.Shared/Holopad/HolopadComponent.cs
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
using Content.Shared.Telephone;
|
||||||
|
using Robust.Shared.GameStates;
|
||||||
|
using Robust.Shared.Prototypes;
|
||||||
|
using Robust.Shared.Serialization;
|
||||||
|
|
||||||
|
namespace Content.Shared.Holopad;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Holds data pertaining to holopads
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Holopads also require a <see cref="TelephoneComponent"/> to function
|
||||||
|
/// </remarks>
|
||||||
|
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
|
||||||
|
[Access(typeof(SharedHolopadSystem))]
|
||||||
|
public sealed partial class HolopadComponent : Component
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The entity being projected by the holopad
|
||||||
|
/// </summary>
|
||||||
|
[ViewVariables]
|
||||||
|
public Entity<HolopadHologramComponent>? Hologram;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The entity using the holopad
|
||||||
|
/// </summary>
|
||||||
|
[ViewVariables]
|
||||||
|
public Entity<HolopadUserComponent>? User;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Proto ID for the user's hologram
|
||||||
|
/// </summary>
|
||||||
|
[DataField]
|
||||||
|
public EntProtoId? HologramProtoId;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The entity that has locked out the controls of this device
|
||||||
|
/// </summary>
|
||||||
|
[ViewVariables, AutoNetworkedField]
|
||||||
|
public EntityUid? ControlLockoutOwner = null;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The game tick the control lockout was initiated
|
||||||
|
/// </summary>
|
||||||
|
[ViewVariables, AutoNetworkedField]
|
||||||
|
public TimeSpan ControlLockoutStartTime;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The duration that the control lockout will last in seconds
|
||||||
|
/// </summary>
|
||||||
|
[DataField]
|
||||||
|
public float ControlLockoutDuration { get; private set; } = 90f;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The duration before the controls can be lockout again in seconds
|
||||||
|
/// </summary>
|
||||||
|
[DataField]
|
||||||
|
public float ControlLockoutCoolDown { get; private set; } = 180f;
|
||||||
|
}
|
||||||
|
|
||||||
|
#region: Event messages
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Data from by the server to the client for the holopad UI
|
||||||
|
/// </summary>
|
||||||
|
[Serializable, NetSerializable]
|
||||||
|
public sealed class HolopadBoundInterfaceState : BoundUserInterfaceState
|
||||||
|
{
|
||||||
|
public readonly Dictionary<NetEntity, string> Holopads;
|
||||||
|
|
||||||
|
public HolopadBoundInterfaceState(Dictionary<NetEntity, string> holopads)
|
||||||
|
{
|
||||||
|
Holopads = holopads;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Triggers the server to send updated power monitoring console data to the client for the single player session
|
||||||
|
/// </summary>
|
||||||
|
[Serializable, NetSerializable]
|
||||||
|
public sealed class HolopadStartNewCallMessage : BoundUserInterfaceMessage
|
||||||
|
{
|
||||||
|
public readonly NetEntity Receiver;
|
||||||
|
|
||||||
|
public HolopadStartNewCallMessage(NetEntity receiver)
|
||||||
|
{
|
||||||
|
Receiver = receiver;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Triggers the server to send updated power monitoring console data to the client for the single player session
|
||||||
|
/// </summary>
|
||||||
|
[Serializable, NetSerializable]
|
||||||
|
public sealed class HolopadAnswerCallMessage : BoundUserInterfaceMessage { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Triggers the server to send updated power monitoring console data to the client for the single player session
|
||||||
|
/// </summary>
|
||||||
|
[Serializable, NetSerializable]
|
||||||
|
public sealed class HolopadEndCallMessage : BoundUserInterfaceMessage { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Triggers the server to send updated power monitoring console data to the client for the single player session
|
||||||
|
/// </summary>
|
||||||
|
[Serializable, NetSerializable]
|
||||||
|
public sealed class HolopadStartBroadcastMessage : BoundUserInterfaceMessage { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Triggers the server to send updated power monitoring console data to the client for the single player session
|
||||||
|
/// </summary>
|
||||||
|
[Serializable, NetSerializable]
|
||||||
|
public sealed class HolopadActivateProjectorMessage : BoundUserInterfaceMessage { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Triggers the server to send updated power monitoring console data to the client for the single player session
|
||||||
|
/// </summary>
|
||||||
|
[Serializable, NetSerializable]
|
||||||
|
public sealed class HolopadStationAiRequestMessage : BoundUserInterfaceMessage { }
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Key to the Holopad UI
|
||||||
|
/// </summary>
|
||||||
|
[Serializable, NetSerializable]
|
||||||
|
public enum HolopadUiKey : byte
|
||||||
|
{
|
||||||
|
InteractionWindow,
|
||||||
|
InteractionWindowForAi,
|
||||||
|
AiActionWindow,
|
||||||
|
AiRequestWindow
|
||||||
|
}
|
||||||
71
Content.Shared/Holopad/HolopadHologramComponent.cs
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
using Robust.Shared.GameStates;
|
||||||
|
using System.Numerics;
|
||||||
|
|
||||||
|
namespace Content.Shared.Holopad;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Holds data pertaining to holopad holograms
|
||||||
|
/// </summary>
|
||||||
|
[RegisterComponent, NetworkedComponent]
|
||||||
|
public sealed partial class HolopadHologramComponent : Component
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Default RSI path
|
||||||
|
/// </summary>
|
||||||
|
[DataField]
|
||||||
|
public string RsiPath = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Default RSI state
|
||||||
|
/// </summary>
|
||||||
|
[DataField]
|
||||||
|
public string RsiState = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Name of the shader to use
|
||||||
|
/// </summary>
|
||||||
|
[DataField]
|
||||||
|
public string ShaderName = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The primary color
|
||||||
|
/// </summary>
|
||||||
|
[DataField]
|
||||||
|
public Color Color1 = Color.White;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The secondary color
|
||||||
|
/// </summary>
|
||||||
|
[DataField]
|
||||||
|
public Color Color2 = Color.White;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The shared color alpha
|
||||||
|
/// </summary>
|
||||||
|
[DataField]
|
||||||
|
public float Alpha = 1f;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The color brightness
|
||||||
|
/// </summary>
|
||||||
|
[DataField]
|
||||||
|
public float Intensity = 1f;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The scroll rate of the hologram shader
|
||||||
|
/// </summary>
|
||||||
|
[DataField]
|
||||||
|
public float ScrollRate = 1f;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The sprite offset
|
||||||
|
/// </summary>
|
||||||
|
[DataField]
|
||||||
|
public Vector2 Offset = new Vector2();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A user that are linked to this hologram
|
||||||
|
/// </summary>
|
||||||
|
[ViewVariables]
|
||||||
|
public Entity<HolopadComponent>? LinkedHolopad;
|
||||||
|
}
|
||||||
104
Content.Shared/Holopad/HolopadUserComponent.cs
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
using Robust.Shared.GameStates;
|
||||||
|
using Robust.Shared.Serialization;
|
||||||
|
|
||||||
|
namespace Content.Shared.Holopad;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Holds data pertaining to entities that are using holopads
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This component is added and removed automatically from entities
|
||||||
|
/// </remarks>
|
||||||
|
[RegisterComponent, NetworkedComponent]
|
||||||
|
[Access(typeof(SharedHolopadSystem))]
|
||||||
|
public sealed partial class HolopadUserComponent : Component
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A list of holopads that the user is interacting with
|
||||||
|
/// </summary>
|
||||||
|
[ViewVariables]
|
||||||
|
public HashSet<Entity<HolopadComponent>> LinkedHolopads = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A networked event raised when the visual state of a hologram is being updated
|
||||||
|
/// </summary>
|
||||||
|
[Serializable, NetSerializable]
|
||||||
|
public sealed class HolopadHologramVisualsUpdateEvent : EntityEventArgs
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The hologram being updated
|
||||||
|
/// </summary>
|
||||||
|
public readonly NetEntity Hologram;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The target the hologram is copying
|
||||||
|
/// </summary>
|
||||||
|
public readonly NetEntity? Target;
|
||||||
|
|
||||||
|
public HolopadHologramVisualsUpdateEvent(NetEntity hologram, NetEntity? target = null)
|
||||||
|
{
|
||||||
|
Hologram = hologram;
|
||||||
|
Target = target;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A networked event raised when the visual state of a hologram is being updated
|
||||||
|
/// </summary>
|
||||||
|
[Serializable, NetSerializable]
|
||||||
|
public sealed class HolopadUserTypingChangedEvent : EntityEventArgs
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The hologram being updated
|
||||||
|
/// </summary>
|
||||||
|
public readonly NetEntity User;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The typing indicator state
|
||||||
|
/// </summary>
|
||||||
|
public readonly bool IsTyping;
|
||||||
|
|
||||||
|
public HolopadUserTypingChangedEvent(NetEntity user, bool isTyping)
|
||||||
|
{
|
||||||
|
User = user;
|
||||||
|
IsTyping = isTyping;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A networked event raised by the server to request the current visual state of a target player entity
|
||||||
|
/// </summary>
|
||||||
|
[Serializable, NetSerializable]
|
||||||
|
public sealed class PlayerSpriteStateRequest : EntityEventArgs
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The player entity in question
|
||||||
|
/// </summary>
|
||||||
|
public readonly NetEntity TargetPlayer;
|
||||||
|
|
||||||
|
public PlayerSpriteStateRequest(NetEntity targetPlayer)
|
||||||
|
{
|
||||||
|
TargetPlayer = targetPlayer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The client's response to a <see cref="PlayerSpriteStateRequest"/>
|
||||||
|
/// </summary>
|
||||||
|
[Serializable, NetSerializable]
|
||||||
|
public sealed class PlayerSpriteStateMessage : EntityEventArgs
|
||||||
|
{
|
||||||
|
public readonly NetEntity SpriteEntity;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Data needed to reconstruct the player's sprite component layers
|
||||||
|
/// </summary>
|
||||||
|
public readonly PrototypeLayerData[]? SpriteLayerData;
|
||||||
|
|
||||||
|
public PlayerSpriteStateMessage(NetEntity spriteEntity, PrototypeLayerData[]? spriteLayerData = null)
|
||||||
|
{
|
||||||
|
SpriteEntity = spriteEntity;
|
||||||
|
SpriteLayerData = spriteLayerData;
|
||||||
|
}
|
||||||
|
}
|
||||||
43
Content.Shared/Holopad/SharedHolopadSystem.cs
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
using Robust.Shared.Timing;
|
||||||
|
|
||||||
|
namespace Content.Shared.Holopad;
|
||||||
|
|
||||||
|
public abstract class SharedHolopadSystem : EntitySystem
|
||||||
|
{
|
||||||
|
[Dependency] private readonly IGameTiming _timing = default!;
|
||||||
|
|
||||||
|
public bool IsHolopadControlLocked(Entity<HolopadComponent> entity, EntityUid? user = null)
|
||||||
|
{
|
||||||
|
if (entity.Comp.ControlLockoutStartTime == TimeSpan.Zero)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (entity.Comp.ControlLockoutStartTime + TimeSpan.FromSeconds(entity.Comp.ControlLockoutDuration) < _timing.CurTime)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (entity.Comp.ControlLockoutOwner == null || entity.Comp.ControlLockoutOwner == user)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public TimeSpan GetHolopadControlLockedPeriod(Entity<HolopadComponent> entity)
|
||||||
|
{
|
||||||
|
return entity.Comp.ControlLockoutStartTime + TimeSpan.FromSeconds(entity.Comp.ControlLockoutDuration) - _timing.CurTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsHolopadBroadcastOnCoolDown(Entity<HolopadComponent> entity)
|
||||||
|
{
|
||||||
|
if (entity.Comp.ControlLockoutStartTime == TimeSpan.Zero)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (entity.Comp.ControlLockoutStartTime + TimeSpan.FromSeconds(entity.Comp.ControlLockoutCoolDown) < _timing.CurTime)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public TimeSpan GetHolopadBroadcastCoolDown(Entity<HolopadComponent> entity)
|
||||||
|
{
|
||||||
|
return entity.Comp.ControlLockoutStartTime + TimeSpan.FromSeconds(entity.Comp.ControlLockoutCoolDown) - _timing.CurTime;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,12 +20,15 @@ using Content.Shared.Verbs;
|
|||||||
using Robust.Shared.Audio;
|
using Robust.Shared.Audio;
|
||||||
using Robust.Shared.Audio.Systems;
|
using Robust.Shared.Audio.Systems;
|
||||||
using Robust.Shared.Containers;
|
using Robust.Shared.Containers;
|
||||||
|
using Robust.Shared.GameObjects;
|
||||||
|
using Robust.Shared.Map;
|
||||||
using Robust.Shared.Map.Components;
|
using Robust.Shared.Map.Components;
|
||||||
using Robust.Shared.Network;
|
using Robust.Shared.Network;
|
||||||
using Robust.Shared.Physics;
|
using Robust.Shared.Physics;
|
||||||
using Robust.Shared.Prototypes;
|
using Robust.Shared.Prototypes;
|
||||||
using Robust.Shared.Serialization;
|
using Robust.Shared.Serialization;
|
||||||
using Robust.Shared.Timing;
|
using Robust.Shared.Timing;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
|
||||||
namespace Content.Shared.Silicons.StationAi;
|
namespace Content.Shared.Silicons.StationAi;
|
||||||
|
|
||||||
@@ -68,6 +71,8 @@ public abstract partial class SharedStationAiSystem : EntitySystem
|
|||||||
[ValidatePrototypeId<EntityPrototype>]
|
[ValidatePrototypeId<EntityPrototype>]
|
||||||
private static readonly EntProtoId DefaultAi = "StationAiBrain";
|
private static readonly EntProtoId DefaultAi = "StationAiBrain";
|
||||||
|
|
||||||
|
private const float MaxVisionMultiplier = 5f;
|
||||||
|
|
||||||
public override void Initialize()
|
public override void Initialize()
|
||||||
{
|
{
|
||||||
base.Initialize();
|
base.Initialize();
|
||||||
@@ -344,16 +349,47 @@ public abstract partial class SharedStationAiSystem : EntitySystem
|
|||||||
AttachEye(ent);
|
AttachEye(ent);
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool SetupEye(Entity<StationAiCoreComponent> ent)
|
public void SwitchRemoteEntityMode(Entity<StationAiCoreComponent> ent, bool isRemote)
|
||||||
|
{
|
||||||
|
if (isRemote == ent.Comp.Remote)
|
||||||
|
return;
|
||||||
|
|
||||||
|
ent.Comp.Remote = isRemote;
|
||||||
|
|
||||||
|
EntityCoordinates? coords = ent.Comp.RemoteEntity != null ? Transform(ent.Comp.RemoteEntity.Value).Coordinates : null;
|
||||||
|
|
||||||
|
// Attach new eye
|
||||||
|
ClearEye(ent);
|
||||||
|
|
||||||
|
if (SetupEye(ent, coords))
|
||||||
|
AttachEye(ent);
|
||||||
|
|
||||||
|
// Adjust user FoV
|
||||||
|
var user = GetInsertedAI(ent);
|
||||||
|
|
||||||
|
if (TryComp<EyeComponent>(user, out var eye))
|
||||||
|
_eye.SetDrawFov(user.Value, !isRemote);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool SetupEye(Entity<StationAiCoreComponent> ent, EntityCoordinates? coords = null)
|
||||||
{
|
{
|
||||||
if (_net.IsClient)
|
if (_net.IsClient)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
if (ent.Comp.RemoteEntity != null)
|
if (ent.Comp.RemoteEntity != null)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
if (ent.Comp.RemoteEntityProto != null)
|
var proto = ent.Comp.RemoteEntityProto;
|
||||||
|
|
||||||
|
if (coords == null)
|
||||||
|
coords = Transform(ent.Owner).Coordinates;
|
||||||
|
|
||||||
|
if (!ent.Comp.Remote)
|
||||||
|
proto = ent.Comp.PhysicalEntityProto;
|
||||||
|
|
||||||
|
if (proto != null)
|
||||||
{
|
{
|
||||||
ent.Comp.RemoteEntity = SpawnAtPosition(ent.Comp.RemoteEntityProto, Transform(ent.Owner).Coordinates);
|
ent.Comp.RemoteEntity = SpawnAtPosition(proto, coords.Value);
|
||||||
Dirty(ent);
|
Dirty(ent);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -364,6 +400,7 @@ public abstract partial class SharedStationAiSystem : EntitySystem
|
|||||||
{
|
{
|
||||||
if (_net.IsClient)
|
if (_net.IsClient)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
QueueDel(ent.Comp.RemoteEntity);
|
QueueDel(ent.Comp.RemoteEntity);
|
||||||
ent.Comp.RemoteEntity = null;
|
ent.Comp.RemoteEntity = null;
|
||||||
Dirty(ent);
|
Dirty(ent);
|
||||||
@@ -392,6 +429,17 @@ public abstract partial class SharedStationAiSystem : EntitySystem
|
|||||||
_mover.SetRelay(user, ent.Comp.RemoteEntity.Value);
|
_mover.SetRelay(user, ent.Comp.RemoteEntity.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private EntityUid? GetInsertedAI(Entity<StationAiCoreComponent> ent)
|
||||||
|
{
|
||||||
|
if (!_containers.TryGetContainer(ent.Owner, StationAiHolderComponent.Container, out var container) ||
|
||||||
|
container.ContainedEntities.Count != 1)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return container.ContainedEntities[0];
|
||||||
|
}
|
||||||
|
|
||||||
private void OnAiInsert(Entity<StationAiCoreComponent> ent, ref EntInsertedIntoContainerMessage args)
|
private void OnAiInsert(Entity<StationAiCoreComponent> ent, ref EntInsertedIntoContainerMessage args)
|
||||||
{
|
{
|
||||||
if (args.Container.ID != StationAiCoreComponent.Container)
|
if (args.Container.ID != StationAiCoreComponent.Container)
|
||||||
@@ -400,6 +448,7 @@ public abstract partial class SharedStationAiSystem : EntitySystem
|
|||||||
if (_timing.ApplyingState)
|
if (_timing.ApplyingState)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
ent.Comp.Remote = true;
|
||||||
SetupEye(ent);
|
SetupEye(ent);
|
||||||
|
|
||||||
// Just so text and the likes works properly
|
// Just so text and the likes works properly
|
||||||
@@ -413,6 +462,8 @@ public abstract partial class SharedStationAiSystem : EntitySystem
|
|||||||
if (_timing.ApplyingState)
|
if (_timing.ApplyingState)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
ent.Comp.Remote = true;
|
||||||
|
|
||||||
// Reset name to whatever
|
// Reset name to whatever
|
||||||
_metadata.SetEntityName(ent.Owner, Prototype(ent.Owner)?.Name ?? string.Empty);
|
_metadata.SetEntityName(ent.Owner, Prototype(ent.Owner)?.Name ?? string.Empty);
|
||||||
|
|
||||||
@@ -424,6 +475,7 @@ public abstract partial class SharedStationAiSystem : EntitySystem
|
|||||||
_eye.SetDrawFov(args.Entity, true, eyeComp);
|
_eye.SetDrawFov(args.Entity, true, eyeComp);
|
||||||
_eye.SetTarget(args.Entity, null, eyeComp);
|
_eye.SetTarget(args.Entity, null, eyeComp);
|
||||||
}
|
}
|
||||||
|
|
||||||
ClearEye(ent);
|
ClearEye(ent);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -478,6 +530,36 @@ public abstract partial class SharedStationAiSystem : EntitySystem
|
|||||||
|
|
||||||
return _blocker.CanComplexInteract(entity.Owner);
|
return _blocker.CanComplexInteract(entity.Owner);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool TryGetStationAiCore(Entity<StationAiHeldComponent?> ent, [NotNullWhen(true)] out Entity<StationAiCoreComponent>? parentEnt)
|
||||||
|
{
|
||||||
|
parentEnt = null;
|
||||||
|
var parent = Transform(ent).ParentUid;
|
||||||
|
|
||||||
|
if (!parent.IsValid())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (!TryComp<StationAiCoreComponent>(parent, out var stationAiCore))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
parentEnt = new Entity<StationAiCoreComponent>(parent, stationAiCore);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryGetInsertedAI(Entity<StationAiCoreComponent> ent, [NotNullWhen(true)] out Entity<StationAiHeldComponent>? insertedAi)
|
||||||
|
{
|
||||||
|
insertedAi = null;
|
||||||
|
var insertedEnt = GetInsertedAI(ent);
|
||||||
|
|
||||||
|
if (TryComp<StationAiHeldComponent>(insertedEnt, out var stationAiHeld))
|
||||||
|
{
|
||||||
|
insertedAi = (insertedEnt.Value, stationAiHeld);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed partial class JumpToCoreEvent : InstantActionEvent
|
public sealed partial class JumpToCoreEvent : InstantActionEvent
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ public sealed partial class StationAiCoreComponent : Component
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Can it move its camera around and interact remotely with things.
|
/// Can it move its camera around and interact remotely with things.
|
||||||
|
/// When false, the AI is being projected into a local area, such as a holopad
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[DataField, AutoNetworkedField]
|
[DataField, AutoNetworkedField]
|
||||||
public bool Remote = true;
|
public bool Remote = true;
|
||||||
@@ -25,8 +26,17 @@ public sealed partial class StationAiCoreComponent : Component
|
|||||||
[DataField, AutoNetworkedField]
|
[DataField, AutoNetworkedField]
|
||||||
public EntityUid? RemoteEntity;
|
public EntityUid? RemoteEntity;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Prototype that represents the 'eye' of the AI
|
||||||
|
/// </summary>
|
||||||
[DataField(readOnly: true)]
|
[DataField(readOnly: true)]
|
||||||
public EntProtoId? RemoteEntityProto = "StationAiHolo";
|
public EntProtoId? RemoteEntityProto = "StationAiHolo";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Prototype that represents the physical avatar of the AI
|
||||||
|
/// </summary>
|
||||||
|
[DataField(readOnly: true)]
|
||||||
|
public EntProtoId? PhysicalEntityProto = "StationAiHoloLocal";
|
||||||
|
|
||||||
public const string Container = "station_ai_mind_slot";
|
public const string Container = "station_ai_mind_slot";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,5 +56,11 @@ namespace Content.Shared.Speech
|
|||||||
public float SoundCooldownTime { get; set; } = 0.5f;
|
public float SoundCooldownTime { get; set; } = 0.5f;
|
||||||
|
|
||||||
public TimeSpan LastTimeSoundPlayed = TimeSpan.Zero;
|
public TimeSpan LastTimeSoundPlayed = TimeSpan.Zero;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Additional vertical offset for speech bubbles generated by this entity
|
||||||
|
/// </summary>
|
||||||
|
[DataField]
|
||||||
|
public float SpeechBubbleOffset = 0f;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
39
Content.Shared/Telephone/SharedTelephoneSystem.cs
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace Content.Shared.Telephone;
|
||||||
|
|
||||||
|
public abstract class SharedTelephoneSystem : EntitySystem
|
||||||
|
{
|
||||||
|
public bool IsTelephoneEngaged(Entity<TelephoneComponent> entity)
|
||||||
|
{
|
||||||
|
return entity.Comp.LinkedTelephones.Any();
|
||||||
|
}
|
||||||
|
|
||||||
|
public string GetFormattedCallerIdForEntity(string? presumedName, string? presumedJob, Color fontColor, string fontType = "Default", int fontSize = 12)
|
||||||
|
{
|
||||||
|
var callerId = Loc.GetString("chat-telephone-unknown-caller",
|
||||||
|
("color", fontColor),
|
||||||
|
("fontType", fontType),
|
||||||
|
("fontSize", fontSize));
|
||||||
|
|
||||||
|
if (presumedName == null)
|
||||||
|
return callerId;
|
||||||
|
|
||||||
|
if (presumedJob != null)
|
||||||
|
callerId = Loc.GetString("chat-telephone-caller-id-with-job",
|
||||||
|
("callerName", presumedName),
|
||||||
|
("callerJob", presumedJob),
|
||||||
|
("color", fontColor),
|
||||||
|
("fontType", fontType),
|
||||||
|
("fontSize", fontSize));
|
||||||
|
|
||||||
|
else
|
||||||
|
callerId = Loc.GetString("chat-telephone-caller-id-without-job",
|
||||||
|
("callerName", presumedName),
|
||||||
|
("color", fontColor),
|
||||||
|
("fontType", fontType),
|
||||||
|
("fontSize", fontSize));
|
||||||
|
|
||||||
|
return callerId;
|
||||||
|
}
|
||||||
|
}
|
||||||
203
Content.Shared/Telephone/TelephoneComponent.cs
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
using Content.Shared.Chat;
|
||||||
|
using Robust.Shared.Audio;
|
||||||
|
using Robust.Shared.GameStates;
|
||||||
|
using Robust.Shared.Serialization;
|
||||||
|
|
||||||
|
namespace Content.Shared.Telephone;
|
||||||
|
|
||||||
|
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
|
||||||
|
[Access(typeof(SharedTelephoneSystem))]
|
||||||
|
public sealed partial class TelephoneComponent : Component
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Sets how long the telephone will ring before it automatically hangs up
|
||||||
|
/// </summary>
|
||||||
|
[DataField]
|
||||||
|
public float RingingTimeout = 30;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets how long the telephone can remain idle in-call before it automatically hangs up
|
||||||
|
/// </summary>
|
||||||
|
[DataField]
|
||||||
|
public float IdlingTimeout = 60;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets how long the telephone will stay in the hanging up state before return to idle
|
||||||
|
/// </summary>
|
||||||
|
[DataField]
|
||||||
|
public float HangingUpTimeout = 2;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tone played while the phone is ringing
|
||||||
|
/// </summary>
|
||||||
|
[DataField]
|
||||||
|
public SoundSpecifier? RingTone = null;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the number of seconds before the next ring tone is played
|
||||||
|
/// </summary>
|
||||||
|
[DataField]
|
||||||
|
public float RingInterval = 2f;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The time at which the next tone will be played
|
||||||
|
/// </summary>
|
||||||
|
[DataField]
|
||||||
|
public TimeSpan NextRingToneTime;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The volume at which relayed messages are played
|
||||||
|
/// </summary>
|
||||||
|
[DataField]
|
||||||
|
public TelephoneVolume SpeakerVolume = TelephoneVolume.Whisper;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The range at which the telephone can connect to another
|
||||||
|
/// </summary>
|
||||||
|
[DataField]
|
||||||
|
public TelephoneRange TransmissionRange = TelephoneRange.Grid;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The range at which the telephone picks up voices
|
||||||
|
/// </summary>
|
||||||
|
[DataField]
|
||||||
|
public float ListeningRange = 2;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Specifies whether this telephone require power to fucntion
|
||||||
|
/// </summary>
|
||||||
|
[DataField]
|
||||||
|
public bool RequiresPower = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This telephone does not appear on public telephone directories
|
||||||
|
/// </summary>
|
||||||
|
[DataField]
|
||||||
|
public bool UnlistedNumber = false;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Telephone number for this device
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// For future use - a system for generating and handling telephone numbers has not been implemented yet
|
||||||
|
/// </remarks>
|
||||||
|
[ViewVariables]
|
||||||
|
public int TelephoneNumber = -1;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Linked telephone
|
||||||
|
/// </summary>
|
||||||
|
[ViewVariables]
|
||||||
|
public HashSet<Entity<TelephoneComponent>> LinkedTelephones = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Defines the current state the telephone is in
|
||||||
|
/// </summary>
|
||||||
|
[ViewVariables, AutoNetworkedField]
|
||||||
|
public TelephoneState CurrentState = TelephoneState.Idle;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The game tick the current state started
|
||||||
|
/// </summary>
|
||||||
|
[ViewVariables]
|
||||||
|
public TimeSpan StateStartTime;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets whether the telphone can pick up nearby speech
|
||||||
|
/// </summary>
|
||||||
|
[ViewVariables]
|
||||||
|
public bool Muted = false;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The presumed name and/or job of the last person to call this telephone
|
||||||
|
/// </summary>
|
||||||
|
[ViewVariables, AutoNetworkedField]
|
||||||
|
public (string?, string?) LastCallerId;
|
||||||
|
}
|
||||||
|
|
||||||
|
#region: Telephone events
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Raised when one telephone is attempting to call another
|
||||||
|
/// </summary>
|
||||||
|
[ByRefEvent]
|
||||||
|
public record struct TelephoneCallAttemptEvent(Entity<TelephoneComponent> Source, Entity<TelephoneComponent> Receiver, EntityUid? User)
|
||||||
|
{
|
||||||
|
public bool Cancelled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Raised when a telephone's state changes
|
||||||
|
/// </summary>
|
||||||
|
[ByRefEvent]
|
||||||
|
public record struct TelephoneStateChangeEvent(TelephoneState OldState, TelephoneState NewState);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Raised when communication between one telephone and another begins
|
||||||
|
/// </summary>
|
||||||
|
[ByRefEvent]
|
||||||
|
public record struct TelephoneCallCommencedEvent(Entity<TelephoneComponent> Receiver);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Raised when a telephone hangs up
|
||||||
|
/// </summary>
|
||||||
|
[ByRefEvent]
|
||||||
|
public record struct TelephoneCallEndedEvent();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Raised when a chat message is sent by a telephone to another
|
||||||
|
/// </summary>
|
||||||
|
[ByRefEvent]
|
||||||
|
public readonly record struct TelephoneMessageSentEvent(string Message, MsgChatMessage ChatMsg, EntityUid MessageSource);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Raised when a chat message is received by a telephone from another
|
||||||
|
/// </summary>
|
||||||
|
[ByRefEvent]
|
||||||
|
public readonly record struct TelephoneMessageReceivedEvent(string Message, MsgChatMessage ChatMsg, EntityUid MessageSource, Entity<TelephoneComponent> TelephoneSource);
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Options for tailoring telephone calls
|
||||||
|
/// </summary>
|
||||||
|
[Serializable, NetSerializable]
|
||||||
|
public struct TelephoneCallOptions
|
||||||
|
{
|
||||||
|
public bool ForceConnect; // The source immediately starts a call with the receiver, potentially interrupting a call that is already in progress
|
||||||
|
public bool ForceJoin; // The source smoothly joins a call in progress, or starts a normal call with the receiver if there is none
|
||||||
|
public bool MuteSource; // Chatter from the source is not transmitted - could be used for eavesdropping when combined with 'ForceJoin'
|
||||||
|
public bool MuteReceiver; // Chatter from the receiver is not transmitted - useful for broadcasting messages to multiple receivers
|
||||||
|
}
|
||||||
|
|
||||||
|
[Serializable, NetSerializable]
|
||||||
|
public enum TelephoneVisuals : byte
|
||||||
|
{
|
||||||
|
Key
|
||||||
|
}
|
||||||
|
|
||||||
|
[Serializable, NetSerializable]
|
||||||
|
public enum TelephoneState : byte
|
||||||
|
{
|
||||||
|
Idle,
|
||||||
|
Calling,
|
||||||
|
Ringing,
|
||||||
|
InCall,
|
||||||
|
EndingCall
|
||||||
|
}
|
||||||
|
|
||||||
|
[Serializable, NetSerializable]
|
||||||
|
public enum TelephoneVolume : byte
|
||||||
|
{
|
||||||
|
Whisper,
|
||||||
|
Speak
|
||||||
|
}
|
||||||
|
|
||||||
|
[Serializable, NetSerializable]
|
||||||
|
public enum TelephoneRange : byte
|
||||||
|
{
|
||||||
|
Grid, // Can call grid/map range telephones that are on the same grid
|
||||||
|
Map, // Can call grid/map range telephones that are on the same map
|
||||||
|
Long, // Can only long range telephones that are on a different map
|
||||||
|
Unlimited // Can call any telephone
|
||||||
|
}
|
||||||
@@ -173,6 +173,11 @@
|
|||||||
copyright: "by Ko4erga"
|
copyright: "by Ko4erga"
|
||||||
source: "https://github.com/space-wizards/space-station-14/pull/30431"
|
source: "https://github.com/space-wizards/space-station-14/pull/30431"
|
||||||
|
|
||||||
|
- files: ["double_ring.ogg"]
|
||||||
|
license: "CC0-1.0"
|
||||||
|
copyright: "Created by fspera, converted to OGG and modified by chromiumboy."
|
||||||
|
source: "https://freesound.org/people/fspera/sounds/528111/"
|
||||||
|
|
||||||
- files:
|
- files:
|
||||||
- airlock_emergencyoff.ogg
|
- airlock_emergencyoff.ogg
|
||||||
- airlock_emergencyon.ogg
|
- airlock_emergencyon.ogg
|
||||||
|
|||||||
BIN
Resources/Audio/Machines/double_ring.ogg
Normal file
40
Resources/Locale/en-US/holopad/holopad.ftl
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
# Window headers
|
||||||
|
holopad-window-title = {CAPITALIZE($title)}
|
||||||
|
holopad-window-subtitle = [color=white][bold]Holographic communication system[/bold][/color]
|
||||||
|
holopad-window-options = [color=darkgray][font size=10][italic]Please select an option from the list below[/italic][/font][/color]
|
||||||
|
|
||||||
|
# Call status
|
||||||
|
holopad-window-no-calls-in-progress = No holo-calls in progress
|
||||||
|
holopad-window-incoming-call = Incoming holo-call from:
|
||||||
|
holopad-window-outgoing-call = Attempting to establish a connection...
|
||||||
|
holopad-window-call-in-progress = Holo-call in progress
|
||||||
|
holopad-window-call-ending = Disconnecting...
|
||||||
|
holopad-window-call-rejected = Unable to establish a connection
|
||||||
|
holopad-window-ai-request = Your presence is requested by:
|
||||||
|
holopad-window-emergency-broadcast-in-progress = [color=#cf2f2f][bold]Emergency broadcast in progress[/bold][/color]
|
||||||
|
holopad-window-controls-locked-out = Control of this device has been locked to:
|
||||||
|
holopad-window-controls-unlock-countdown = It will automatically unlock in: {$countdown}
|
||||||
|
|
||||||
|
# Buttons
|
||||||
|
holopad-window-answer-call = Answer call
|
||||||
|
holopad-window-end-call = End call
|
||||||
|
holopad-window-request-station-ai = Request station AI
|
||||||
|
holopad-window-activate-projector = Activate projector
|
||||||
|
holopad-window-emergency-broadcast = Emergency broadcast
|
||||||
|
holopad-window-emergency-broadcast-with-countdown = Emergency broadcast ({$countdown})
|
||||||
|
holopad-window-access-denied = Access denied
|
||||||
|
|
||||||
|
# Contact list
|
||||||
|
holopad-window-select-contact-from-list = Select a contact to initiate a holo-call
|
||||||
|
holopad-window-fetching-contacts-list = No holopads are currently contactable
|
||||||
|
holopad-window-contact-label = {CAPITALIZE($label)}
|
||||||
|
|
||||||
|
# Flavor
|
||||||
|
holopad-window-flavor-left = ⚠ Do not enter while projector is active
|
||||||
|
holopad-window-flavor-right = v3.0.9
|
||||||
|
|
||||||
|
# Holograms
|
||||||
|
holopad-hologram-name = hologram of {THE($name)}
|
||||||
|
|
||||||
|
# Holopad actions
|
||||||
|
activate-holopad-projector-verb = Activate holopad projector
|
||||||
8
Resources/Locale/en-US/telephone/telephone.ftl
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
# Chat window telephone wrap (prefix and postfix)
|
||||||
|
chat-telephone-message-wrap = [color={$color}][bold]{$name}[/bold] {$verb}, [font={$fontType} size={$fontSize}]"{$message}"[/font][/color]
|
||||||
|
chat-telephone-message-wrap-bold = [color={$color}][bold]{$name}[/bold] {$verb}, [font={$fontType} size={$fontSize}][bold]"{$message}"[/bold][/font][/color]
|
||||||
|
|
||||||
|
# Caller ID
|
||||||
|
chat-telephone-unknown-caller = [color={$color}][font={$fontType} size={$fontSize}][bolditalic]Unknown caller[/bolditalic][/font][/color]
|
||||||
|
chat-telephone-caller-id-with-job = [color={$color}][font={$fontType} size={$fontSize}][bold]{CAPITALIZE($callerName)} ({CAPITALIZE($callerJob)})[/bold][/font][/color]
|
||||||
|
chat-telephone-caller-id-without-job = [color={$color}][font={$fontType} size={$fontSize}][bold]{CAPITALIZE($callerName)}[/bold][/font][/color]
|
||||||
@@ -41,6 +41,7 @@ wires-board-name-flatpacker = Flatpacker
|
|||||||
wires-board-name-spaceheater = Space Heater
|
wires-board-name-spaceheater = Space Heater
|
||||||
wires-board-name-jukebox = Jukebox
|
wires-board-name-jukebox = Jukebox
|
||||||
wires-board-name-computer = Computer
|
wires-board-name-computer = Computer
|
||||||
|
wires-board-name-holopad = Holopad
|
||||||
wires-board-name-barsign = Bar Sign
|
wires-board-name-barsign = Bar Sign
|
||||||
|
|
||||||
# names that get displayed in the wire hacking hud & admin logs.
|
# names that get displayed in the wire hacking hud & admin logs.
|
||||||
|
|||||||
@@ -70,6 +70,10 @@
|
|||||||
canShuttle: false
|
canShuttle: false
|
||||||
title: comms-console-announcement-title-station-ai
|
title: comms-console-announcement-title-station-ai
|
||||||
color: "#5ed7aa"
|
color: "#5ed7aa"
|
||||||
|
- type: HolographicAvatar
|
||||||
|
layerData:
|
||||||
|
- sprite: Mobs/Silicon/station_ai.rsi
|
||||||
|
state: default
|
||||||
- type: ShowJobIcons
|
- type: ShowJobIcons
|
||||||
|
|
||||||
- type: entity
|
- type: entity
|
||||||
@@ -364,6 +368,19 @@
|
|||||||
unshaded:
|
unshaded:
|
||||||
Empty: { state: ai_empty }
|
Empty: { state: ai_empty }
|
||||||
Occupied: { state: ai }
|
Occupied: { state: ai }
|
||||||
|
- type: Telephone
|
||||||
|
listeningRange: 0
|
||||||
|
speakerVolume: Speak
|
||||||
|
unlistedNumber: true
|
||||||
|
requiresPower: false
|
||||||
|
- type: Holopad
|
||||||
|
- type: StationAiWhitelist
|
||||||
|
- type: UserInterface
|
||||||
|
interfaces:
|
||||||
|
enum.HolopadUiKey.AiRequestWindow:
|
||||||
|
type: HolopadBoundUserInterface
|
||||||
|
enum.HolopadUiKey.AiActionWindow:
|
||||||
|
type: HolopadBoundUserInterface
|
||||||
|
|
||||||
# The job-ready version of an AI spawn.
|
# The job-ready version of an AI spawn.
|
||||||
- type: entity
|
- type: entity
|
||||||
@@ -446,6 +463,28 @@
|
|||||||
shader: unshaded
|
shader: unshaded
|
||||||
map: ["base"]
|
map: ["base"]
|
||||||
|
|
||||||
|
# The holographic representation of the AI that is projected from a holopad.
|
||||||
|
- type: entity
|
||||||
|
id: StationAiHoloLocal
|
||||||
|
name: AI hologram
|
||||||
|
description: A holographic representation of an AI.
|
||||||
|
categories: [ HideSpawnMenu ]
|
||||||
|
suffix: DO NOT MAP
|
||||||
|
components:
|
||||||
|
- type: Transform
|
||||||
|
anchored: true
|
||||||
|
- type: WarpPoint
|
||||||
|
follow: true
|
||||||
|
- type: Eye
|
||||||
|
- type: ContentEye
|
||||||
|
- type: Examiner
|
||||||
|
- type: Actions
|
||||||
|
- type: Alerts
|
||||||
|
- type: FTLSmashImmune
|
||||||
|
- type: CargoSellBlacklist
|
||||||
|
- type: StationAiVision
|
||||||
|
range: 20
|
||||||
|
|
||||||
# Borgs
|
# Borgs
|
||||||
- type: entity
|
- type: entity
|
||||||
id: PlayerBorgBattery
|
id: PlayerBorgBattery
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
- type: entity
|
||||||
|
id: HolopadMachineCircuitboard
|
||||||
|
parent: BaseMachineCircuitboard
|
||||||
|
name: holopad machine board
|
||||||
|
description: A machine printed circuit board for a holopad.
|
||||||
|
components:
|
||||||
|
- type: MachineBoard
|
||||||
|
prototype: Holopad
|
||||||
|
stackRequirements:
|
||||||
|
Capacitor: 4
|
||||||
|
Cable: 4
|
||||||
|
Glass: 2
|
||||||
158
Resources/Prototypes/Entities/Structures/Machines/holopad.yml
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
- type: entity
|
||||||
|
parent: [ BaseMachinePowered, ConstructibleMachine ]
|
||||||
|
id: Holopad
|
||||||
|
name: holopad
|
||||||
|
description: "A floor-mounted device for projecting holographic images."
|
||||||
|
components:
|
||||||
|
- type: Transform
|
||||||
|
anchored: true
|
||||||
|
- type: Fixtures
|
||||||
|
fixtures:
|
||||||
|
fix1:
|
||||||
|
shape:
|
||||||
|
!type:PhysShapeCircle
|
||||||
|
radius: 0.25
|
||||||
|
mask:
|
||||||
|
- SubfloorMask
|
||||||
|
layer:
|
||||||
|
- LowImpassable
|
||||||
|
hard: false
|
||||||
|
- type: ApcPowerReceiver
|
||||||
|
powerLoad: 300
|
||||||
|
- type: StationAiVision
|
||||||
|
- type: Sprite
|
||||||
|
sprite: Structures/Machines/holopad.rsi
|
||||||
|
snapCardinals: true
|
||||||
|
layers:
|
||||||
|
- state: base
|
||||||
|
- map: [ "lights" ]
|
||||||
|
state: blank
|
||||||
|
shader: unshaded
|
||||||
|
- map: [ "enum.PowerDeviceVisualLayers.Powered" ]
|
||||||
|
state: unpowered
|
||||||
|
- map: [ "enum.WiresVisualLayers.MaintenancePanel" ]
|
||||||
|
state: panel_open
|
||||||
|
- type: Appearance
|
||||||
|
- type: GenericVisualizer
|
||||||
|
visuals:
|
||||||
|
enum.TelephoneVisuals.Key:
|
||||||
|
lights:
|
||||||
|
Idle: { state: blank }
|
||||||
|
Calling: { state: lights_calling }
|
||||||
|
Ringing: { state: lights_ringing }
|
||||||
|
InCall: { state: lights_in_call }
|
||||||
|
EndingCall: { state: lights_hanging_up }
|
||||||
|
enum.PowerDeviceVisuals.Powered:
|
||||||
|
enum.PowerDeviceVisualLayers.Powered:
|
||||||
|
False: { visible: true }
|
||||||
|
True: { visible: false }
|
||||||
|
enum.WiresVisuals.MaintenancePanelState:
|
||||||
|
enum.WiresVisualLayers.MaintenancePanel:
|
||||||
|
True: { visible: false }
|
||||||
|
False: { visible: true }
|
||||||
|
- type: Machine
|
||||||
|
board: HolopadMachineCircuitboard
|
||||||
|
- type: StationAiWhitelist
|
||||||
|
- type: PointLight
|
||||||
|
radius: 1.3
|
||||||
|
energy: 1.8
|
||||||
|
color: "#afe1fe"
|
||||||
|
enabled: false
|
||||||
|
- type: AmbientSound
|
||||||
|
enabled: false
|
||||||
|
volume: -5
|
||||||
|
range: 3
|
||||||
|
sound:
|
||||||
|
path: /Audio/Ambience/Objects/buzzing.ogg
|
||||||
|
- type: Holopad
|
||||||
|
hologramProtoId: HolopadHologram
|
||||||
|
- type: Speech
|
||||||
|
speechVerb: Robotic
|
||||||
|
speechSounds: Borg
|
||||||
|
speechBubbleOffset: 0.45
|
||||||
|
- type: Telephone
|
||||||
|
transmissionRange: Map
|
||||||
|
ringTone: /Audio/Machines/double_ring.ogg
|
||||||
|
listeningRange: 4
|
||||||
|
speakerVolume: Speak
|
||||||
|
- type: AccessReader
|
||||||
|
access: [[ "Command" ]]
|
||||||
|
- type: ActivatableUI
|
||||||
|
key: enum.HolopadUiKey.InteractionWindow
|
||||||
|
- type: ActivatableUIRequiresPower
|
||||||
|
- type: UserInterface
|
||||||
|
interfaces:
|
||||||
|
enum.HolopadUiKey.InteractionWindow:
|
||||||
|
type: HolopadBoundUserInterface
|
||||||
|
enum.WiresUiKey.Key:
|
||||||
|
type: WiresBoundUserInterface
|
||||||
|
- type: WiresPanel
|
||||||
|
- type: WiresVisuals
|
||||||
|
- type: Wires
|
||||||
|
boardName: wires-board-name-holopad
|
||||||
|
layoutId: Holopad
|
||||||
|
- type: Destructible
|
||||||
|
thresholds:
|
||||||
|
- trigger:
|
||||||
|
!type:DamageTrigger
|
||||||
|
damage: 100
|
||||||
|
behaviors:
|
||||||
|
- !type:PlaySoundBehavior
|
||||||
|
sound:
|
||||||
|
collection: MetalBreak
|
||||||
|
- !type:ChangeConstructionNodeBehavior
|
||||||
|
node: machineFrame
|
||||||
|
- !type:DoActsBehavior
|
||||||
|
acts: ["Destruction"]
|
||||||
|
|
||||||
|
- type: entity
|
||||||
|
name: long-range holopad
|
||||||
|
description: "A floor-mounted device for projecting holographic images to other devices that are far away."
|
||||||
|
parent: Holopad
|
||||||
|
id: HolopadLongRange
|
||||||
|
suffix: For calls between maps
|
||||||
|
components:
|
||||||
|
- type: Telephone
|
||||||
|
transmissionRange: Long
|
||||||
|
|
||||||
|
- type: entity
|
||||||
|
name: quantum entangling holopad
|
||||||
|
description: "An experimental floor-mounted device for projecting holographic images at extreme distances."
|
||||||
|
parent: Holopad
|
||||||
|
id: HolopadUnlimitedRange
|
||||||
|
suffix: Unlimited range
|
||||||
|
components:
|
||||||
|
- type: Telephone
|
||||||
|
transmissionRange: Unlimited
|
||||||
|
- type: AccessReader
|
||||||
|
access: [[]]
|
||||||
|
|
||||||
|
# These are spawned by holopads
|
||||||
|
- type: entity
|
||||||
|
id: HolopadHologram
|
||||||
|
categories: [ HideSpawnMenu ]
|
||||||
|
suffix: DO NOT MAP
|
||||||
|
components:
|
||||||
|
- type: Transform
|
||||||
|
anchored: true
|
||||||
|
- type: Sprite
|
||||||
|
noRot: true
|
||||||
|
drawdepth: Mobs
|
||||||
|
offset: -0.02, 0.45
|
||||||
|
overrideDir: South
|
||||||
|
enableOverrideDir: true
|
||||||
|
- type: Appearance
|
||||||
|
- type: TypingIndicator
|
||||||
|
proto: robot
|
||||||
|
- type: HolopadHologram
|
||||||
|
rsiPath: Structures/Machines/holopad.rsi
|
||||||
|
rsiState: icon_in_call
|
||||||
|
shaderName: Hologram
|
||||||
|
color1: "#65b8e2"
|
||||||
|
color2: "#3a6981"
|
||||||
|
alpha: 0.9
|
||||||
|
intensity: 2
|
||||||
|
scrollRate: 0.125
|
||||||
|
- type: Tag
|
||||||
|
tags:
|
||||||
|
- HideContextMenu
|
||||||
@@ -104,3 +104,8 @@
|
|||||||
id: Cataracts
|
id: Cataracts
|
||||||
kind: source
|
kind: source
|
||||||
path: "/Textures/Shaders/cataracts.swsl"
|
path: "/Textures/Shaders/cataracts.swsl"
|
||||||
|
|
||||||
|
- type: shader
|
||||||
|
id: Hologram
|
||||||
|
kind: source
|
||||||
|
path: "/Textures/Shaders/hologram.swsl"
|
||||||
@@ -190,6 +190,14 @@
|
|||||||
- !type:PowerWireAction
|
- !type:PowerWireAction
|
||||||
- !type:AiInteractWireAction
|
- !type:AiInteractWireAction
|
||||||
|
|
||||||
|
- type: wireLayout
|
||||||
|
id: Holopad
|
||||||
|
dummyWires: 2
|
||||||
|
wires:
|
||||||
|
- !type:PowerWireAction
|
||||||
|
- !type:AiInteractWireAction
|
||||||
|
- !type:AiVisionWireAction
|
||||||
|
|
||||||
- type: wireLayout
|
- type: wireLayout
|
||||||
id: BarSign
|
id: BarSign
|
||||||
dummyWires: 2
|
dummyWires: 2
|
||||||
|
|||||||
23
Resources/Textures/Shaders/hologram.swsl
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
light_mode unshaded;
|
||||||
|
|
||||||
|
uniform highp vec3 color1;
|
||||||
|
uniform highp vec3 color2;
|
||||||
|
uniform highp float alpha;
|
||||||
|
uniform highp float intensity;
|
||||||
|
uniform highp float texHeight;
|
||||||
|
uniform highp float t;
|
||||||
|
|
||||||
|
const highp float PI = 3.14159265;
|
||||||
|
|
||||||
|
void fragment() {
|
||||||
|
highp vec4 base = texture2D(TEXTURE, UV);
|
||||||
|
highp float bw = zGrayscale(base.rgb * intensity);
|
||||||
|
highp vec4 color = vec4(vec3(color1), alpha);
|
||||||
|
|
||||||
|
if (sin(PI * (UV.y + t) * texHeight) < 0.0)
|
||||||
|
{
|
||||||
|
color = vec4(vec3(color2), alpha);
|
||||||
|
}
|
||||||
|
|
||||||
|
COLOR = vec4(vec3(bw), base.a) * color;
|
||||||
|
}
|
||||||
BIN
Resources/Textures/Structures/Machines/holopad.rsi/base.png
Normal file
|
After Width: | Height: | Size: 457 B |
BIN
Resources/Textures/Structures/Machines/holopad.rsi/blank.png
Normal file
|
After Width: | Height: | Size: 83 B |
|
After Width: | Height: | Size: 414 B |
|
After Width: | Height: | Size: 425 B |
|
After Width: | Height: | Size: 432 B |
|
After Width: | Height: | Size: 326 B |
|
After Width: | Height: | Size: 4.3 KiB |
100
Resources/Textures/Structures/Machines/holopad.rsi/meta.json
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"license": "CC-BY-SA-3.0",
|
||||||
|
"copyright": "Taken from tgstation at https://github.com/tgstation/tgstation/pull/80025/commits/f0cc8856d4c1b6b3933524a2d37581cc81c3c05b, /icons/obj/machines/floor.dmi. Edited by chromiumboy",
|
||||||
|
"size": {
|
||||||
|
"x": 32,
|
||||||
|
"y": 32
|
||||||
|
},
|
||||||
|
"states": [
|
||||||
|
{
|
||||||
|
"name": "base"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "unpowered"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "panel_open"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "blank"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "icon_in_call"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "lights_calling",
|
||||||
|
"delays": [
|
||||||
|
[
|
||||||
|
0.2,
|
||||||
|
0.1,
|
||||||
|
0.1,
|
||||||
|
0.1,
|
||||||
|
0.1,
|
||||||
|
0.5,
|
||||||
|
0.1,
|
||||||
|
0.1,
|
||||||
|
0.1,
|
||||||
|
0.1
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "lights_in_call",
|
||||||
|
"delays": [
|
||||||
|
[
|
||||||
|
0.1,
|
||||||
|
0.1,
|
||||||
|
0.1,
|
||||||
|
0.1,
|
||||||
|
0.1,
|
||||||
|
0.1,
|
||||||
|
0.1,
|
||||||
|
0.1
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "lights_ringing",
|
||||||
|
"delays": [
|
||||||
|
[
|
||||||
|
0.1,
|
||||||
|
0.1,
|
||||||
|
0.1,
|
||||||
|
0.1,
|
||||||
|
0.1,
|
||||||
|
0.2,
|
||||||
|
0.1,
|
||||||
|
0.1,
|
||||||
|
0.1,
|
||||||
|
0.1,
|
||||||
|
0.1,
|
||||||
|
0.8
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "lights_hanging_up",
|
||||||
|
"delays": [
|
||||||
|
[
|
||||||
|
0.1,
|
||||||
|
0.1,
|
||||||
|
0.1,
|
||||||
|
0.1,
|
||||||
|
0.1,
|
||||||
|
0.1,
|
||||||
|
0.1,
|
||||||
|
0.1,
|
||||||
|
0.1,
|
||||||
|
0.1,
|
||||||
|
0.1,
|
||||||
|
0.1,
|
||||||
|
0.1,
|
||||||
|
0.1,
|
||||||
|
0.1,
|
||||||
|
99
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 221 B |
BIN
Resources/Textures/Structures/Machines/holopad.rsi/unpowered.png
Normal file
|
After Width: | Height: | Size: 441 B |