Minor improvements for holopads (#40444)

* Initial commit

* Fixing merge conflict

* Merge conflict fixed

* Additional fixes

* Reverted change

* Serialization fix

* Holopad UIs will update when one is deleted or changes map

* Minor clean up

* Removed obsolete variables

* Addressed review comments
This commit is contained in:
chromiumboy 2026-05-18 08:23:41 -05:00 • committed by GitHub
parent 4345bac4ab
commit af2d5d401a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 261 additions and 181 deletions

View file

@ -1,6 +1,5 @@
<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">
@ -10,7 +9,7 @@
<!-- Header text -->
<controls:StripeBack>
<PanelContainer>
<RichTextLabel Name="EmergencyBroadcastText" VerticalAlignment="Center" HorizontalAlignment="Center" Margin="10 10 10 10" ReservesSpace="False"/>
<RichTextLabel Text="{Loc 'holopad-window-emergency-broadcast-in-progress'}" VerticalAlignment="Center" HorizontalAlignment="Center" Margin="10 10 10 10" ReservesSpace="False"/>
</PanelContainer>
</controls:StripeBack>
@ -52,8 +51,8 @@
<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"/>
<RichTextLabel Text="{Loc 'holopad-window-subtitle'}" HorizontalAlignment="Center" Margin="0 5 0 0"/>
<RichTextLabel Text="{Loc 'holopad-window-options'}" HorizontalAlignment="Center" Margin="0 0 0 5"/>
</BoxContainer>
</PanelContainer>
</controls:StripeBack>

View file

@ -10,7 +10,6 @@ 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;
@ -19,22 +18,18 @@ public sealed partial class HolopadWindow : FancyWindow
{
[Dependency] private IEntityManager _entManager = default!;
[Dependency] private IPlayerManager _playerManager = default!;
[Dependency] private ILogManager _logManager = default!;
[Dependency] private IGameTiming _timing = default!;
private readonly SharedHolopadSystem _holopadSystem = default!;
private readonly SharedTelephoneSystem _telephoneSystem = default!;
private readonly AccessReaderSystem _accessReaderSystem = default!;
private readonly PopupSystem _popupSystem = default!;
private readonly ISawmill _sawmill = 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);
private string _currentSearch = string.Empty;
public event Action<NetEntity>? SendHolopadStartNewCallMessageAction;
public event Action? SendHolopadAnswerCallMessageAction;
@ -43,6 +38,9 @@ public sealed partial class HolopadWindow : FancyWindow
public event Action? SendHolopadActivateProjectorMessageAction;
public event Action? SendHolopadRequestStationAiMessageAction;
private TimeSpan _updateDelay = TimeSpan.FromSeconds(0.25f);
private TimeSpan _nextUpdate;
public HolopadWindow()
{
RobustXamlLoader.Load(this);
@ -52,8 +50,7 @@ public sealed partial class HolopadWindow : FancyWindow
_telephoneSystem = _entManager.System<SharedTelephoneSystem>();
_accessReaderSystem = _entManager.System<AccessReaderSystem>();
_popupSystem = _entManager.System<PopupSystem>();
_buttonUnlockTime = _timing.CurTime + _buttonUnlockDelay;
_sawmill = _logManager.GetSawmill("Holopad");
// Assign button actions
AnswerCallButton.OnPressed += args => { OnHolopadAnswerCallMessage(); };
@ -78,10 +75,6 @@ public sealed partial class HolopadWindow : FancyWindow
{
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
@ -166,16 +159,28 @@ public sealed partial class HolopadWindow : FancyWindow
public void UpdateState(Dictionary<NetEntity, string> holopads)
{
if (_owner == null || !_entManager.TryGetComponent<TelephoneComponent>(_owner.Value, out var telephone))
if (!_entManager.TryGetComponent<TelephoneComponent>(_owner, out var telephone))
return;
// Caller ID text
var callerId = _telephoneSystem.GetFormattedCallerIdForEntity(telephone.LastCallerId.Item1, telephone.LastCallerId.Item2, Color.LightGray, "Default", 11);
var holoapdId = _telephoneSystem.GetFormattedDeviceIdForEntity(telephone.LastCallerId.Item3, Color.LightGray, "Default", 11);
// Caller and holopad ID text
var callerId = _telephoneSystem.GetFormattedCallerIdForEntity(telephone.LastCallerId?.CallerId, telephone.LastCallerId?.CallerJob, Color.LightGray, "Default", 11);
var holopadId = _telephoneSystem.GetFormattedDeviceIdForEntity(telephone.LastCallerId?.DeviceId, Color.LightGray, "Default", 11);
CallerIdText.SetMessage(FormattedMessage.FromMarkupPermissive(callerId));
HolopadIdText.SetMessage(FormattedMessage.FromMarkupPermissive(holoapdId));
LockOutIdText.SetMessage(FormattedMessage.FromMarkupPermissive(callerId));
if (!FormattedMessage.TryFromMarkup(callerId, out var callerIdMsg))
{
callerIdMsg = FormattedMessage.FromMarkupPermissive(callerId);
_sawmill.Error($"CallerId markup text was incorrectly formatted: {callerIdMsg}");
}
if (!FormattedMessage.TryFromMarkup(holopadId, out var holopadIdMsg))
{
holopadIdMsg = FormattedMessage.FromMarkupPermissive(holopadId);
_sawmill.Error($"HolopadId markup text was incorrectly formatted: {holopadIdMsg}");
}
CallerIdText.SetMessage(callerIdMsg);
LockOutIdText.SetMessage(callerIdMsg);
HolopadIdText.SetMessage(holopadIdMsg);
// Sort holopads alphabetically
var holopadArray = holopads.ToArray();
@ -183,7 +188,9 @@ public sealed partial class HolopadWindow : FancyWindow
// 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++)
@ -213,55 +220,57 @@ public sealed partial class HolopadWindow : FancyWindow
private void UpdateAppearance()
{
if (_owner == null || !_entManager.TryGetComponent<TelephoneComponent>(_owner.Value, out var telephone))
if (!_entManager.TryGetComponent<TelephoneComponent>(_owner, out var telephone))
return;
if (_owner == null || !_entManager.TryGetComponent<HolopadComponent>(_owner.Value, out var holopad))
if (!_entManager.TryGetComponent<HolopadComponent>(_owner, out var holopad))
return;
var hasBroadcastAccess = !_holopadSystem.IsHolopadBroadcastOnCoolDown((_owner.Value, holopad));
var broadcastOnCooldown = _holopadSystem.IsHolopadBroadcastOnCoolDown((_owner.Value, holopad));
var localPlayer = _playerManager.LocalSession?.AttachedEntity;
// Update container visibility
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)
// Update contact button visibility
if (SearchLineEdit.Text != _currentSearch)
{
_previousState = _currentState;
_currentState = telephone.CurrentState;
_buttonUnlockTime = _timing.CurTime + _buttonUnlockDelay;
_currentSearch = SearchLineEdit.Text;
foreach (var child in ContactsList.Children)
{
if (child is not HolopadContactButton contactButton)
continue;
var passesFilter = string.IsNullOrEmpty(SearchLineEdit.Text) ||
contactButton.Text?.Contains(SearchLineEdit.Text, StringComparison.CurrentCultureIgnoreCase) == true;
contactButton.Visible = passesFilter;
}
}
var lockButtons = _timing.CurTime < _buttonUnlockTime;
// Make / update required children
foreach (var child in ContactsList.Children)
// Update timers
if (ControlsContainer.Visible)
{
if (child is not HolopadContactButton contactButton)
continue;
var cooldown = _holopadSystem.GetHolopadBroadcastCoolDown((_owner.Value, holopad));
var cooldownString = $"{cooldown.Minutes:00}:{cooldown.Seconds:00}";
var passesFilter = string.IsNullOrEmpty(SearchLineEdit.Text) ||
contactButton.Text?.Contains(SearchLineEdit.Text, StringComparison.CurrentCultureIgnoreCase) == true;
contactButton.Visible = passesFilter;
contactButton.Disabled = (_currentState != TelephoneState.Idle || lockButtons);
StartBroadcastButton.Text = broadcastOnCooldown
? Loc.GetString("holopad-window-emergency-broadcast-with-countdown", ("countdown", cooldownString))
: Loc.GetString("holopad-window-emergency-broadcast");
}
// Update control text
var cooldown = _holopadSystem.GetHolopadBroadcastCoolDown((_owner.Value, holopad));
var cooldownString = $"{cooldown.Minutes:00}:{cooldown.Seconds:00}";
if (ControlsLockOutContainer.Visible)
{
var lockout = _holopadSystem.GetHolopadControlLockedPeriod((_owner.Value, holopad));
var lockoutString = $"{lockout.Minutes:00}:{lockout.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");
LockOutCountDownText.Text = Loc.GetString("holopad-window-controls-unlock-countdown", ("countdown", lockoutString));
}
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)
// Update call status text
switch (telephone.CurrentState)
{
case TelephoneState.Idle:
CallStatusText.Text = Loc.GetString("holopad-window-no-calls-in-progress"); break;
@ -277,7 +286,7 @@ public sealed partial class HolopadWindow : FancyWindow
CallStatusText.Text = Loc.GetString("holopad-window-call-in-progress"); break;
case TelephoneState.EndingCall:
if (_previousState == TelephoneState.Calling || _previousState == TelephoneState.Idle)
if (telephone.PreviousState == TelephoneState.Calling || telephone.PreviousState == TelephoneState.Idle)
CallStatusText.Text = Loc.GetString("holopad-window-call-rejected");
else
CallStatusText.Text = Loc.GetString("holopad-window-call-ending");
@ -285,31 +294,29 @@ public sealed partial class HolopadWindow : FancyWindow
}
// 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);
AnswerCallButton.Disabled = telephone.CurrentState is not TelephoneState.Ringing;
EndCallButton.Disabled = telephone.CurrentState is TelephoneState.Idle or TelephoneState.EndingCall;
StartBroadcastButton.Disabled = telephone.CurrentState is not TelephoneState.Idle || broadcastOnCooldown;
RequestStationAiButton.Disabled = telephone.CurrentState is not TelephoneState.Idle;
ActivateProjectorButton.Disabled = telephone.CurrentState is not TelephoneState.Idle;
// Update control visibility
FetchingAvailableHolopadsContainer.Visible = (ContactsList.ChildCount == 0);
ActiveCallControlsContainer.Visible = (_currentState != TelephoneState.Idle || _currentUiKey == HolopadUiKey.AiRequestWindow);
FetchingAvailableHolopadsContainer.Visible = ContactsList.ChildCount == 0;
ActiveCallControlsContainer.Visible = telephone.CurrentState is not TelephoneState.Idle || _currentUiKey is HolopadUiKey.AiRequestWindow;
CallPlacementControlsContainer.Visible = !ActiveCallControlsContainer.Visible;
CallerIdContainer.Visible = (_currentState == TelephoneState.Ringing);
AnswerCallButton.Visible = (_currentState == TelephoneState.Ringing);
CallerIdContainer.Visible = telephone.CurrentState is TelephoneState.Ringing;
AnswerCallButton.Visible = telephone.CurrentState is TelephoneState.Ringing;
}
protected override void FrameUpdate(FrameEventArgs args)
{
base.FrameUpdate(args);
_updateTimer += args.DeltaSeconds;
if (_timing.CurTime < _nextUpdate)
return;
if (_updateTimer >= UpdateTime)
{
_updateTimer -= UpdateTime;
UpdateAppearance();
}
_nextUpdate = _timing.CurTime + _updateDelay;
UpdateAppearance();
}
private sealed class HolopadContactButton : Button

View file

@ -42,9 +42,7 @@ public sealed partial class HolopadSystem : SharedHolopadSystem
[Dependency] private IGameTiming _timing = default!;
[Dependency] private PvsOverrideSystem _pvs = default!;
[Dependency] private SharedPowerStateSystem _powerState = default!;
private float _updateTimer = 1.0f;
private const float UpdateTime = 1.0f;
[Dependency] private MetaDataSystem _meta = default!;
public override void Initialize()
{
@ -79,10 +77,10 @@ public sealed partial class HolopadSystem : SharedHolopadSystem
SubscribeLocalEvent<HolopadUserComponent, JumpToCoreEvent>(OnJumpToCore);
SubscribeLocalEvent<HolopadComponent, GetVerbsEvent<AlternativeVerb>>(AddToggleProjectorVerb);
SubscribeLocalEvent<HolopadComponent, EntRemovedFromContainerMessage>(OnAiRemove);
SubscribeLocalEvent<HolopadComponent, EntParentChangedMessage>(OnParentChanged);
SubscribeLocalEvent<HolopadComponent, MapUidChangedEvent>(OnMapUidChanged);
SubscribeLocalEvent<HolopadComponent, PowerChangedEvent>(OnPowerChanged);
SubscribeLocalEvent<HolopadComponent, AnchorStateChangedEvent>(OnAnchorChanged);
SubscribeLocalEvent<HolopadUserComponent, MobStateChangedEvent>(OnMobStateChanged);
}
#region: Holopad UI bound user interface messages
@ -263,6 +261,8 @@ public sealed partial class HolopadSystem : SharedHolopadSystem
SetHolopadAmbientState(holopad, this.IsPowered(holopad, EntityManager));
break;
}
UpdateUIState(holopad);
}
private void OnHoloCallCommenced(Entity<HolopadComponent> source, ref TelephoneCallCommencedEvent args)
@ -315,7 +315,7 @@ public sealed partial class HolopadSystem : SharedHolopadSystem
if (receiverHolopad.Comp.Hologram == null)
continue;
_appearanceSystem.SetData(receiverHolopad.Comp.Hologram.Value.Owner, TypingIndicatorVisuals.State, ev.State);
_appearanceSystem.SetData(receiverHolopad.Comp.Hologram.Value, TypingIndicatorVisuals.State, ev.State);
}
}
}
@ -328,6 +328,8 @@ public sealed partial class HolopadSystem : SharedHolopadSystem
{
if (entity.Comp.User != null)
LinkHolopadToUser(entity, entity.Comp.User.Value);
_meta.AddFlag(entity, MetaDataFlags.ExtraTransformEvents);
}
private void OnHolopadUserInit(Entity<HolopadUserComponent> entity, ref ComponentInit args)
@ -343,6 +345,7 @@ public sealed partial class HolopadSystem : SharedHolopadSystem
ShutDownHolopad(entity);
SetHolopadAmbientState(entity, false);
UpdateAllUIStates();
}
private void OnHolopadUserShutdown(Entity<HolopadUserComponent> entity, ref ComponentShutdown args)
@ -444,15 +447,25 @@ public sealed partial class HolopadSystem : SharedHolopadSystem
_telephoneSystem.EndTelephoneCalls((entity, entityTelephone));
}
private void OnParentChanged(Entity<HolopadComponent> entity, ref EntParentChangedMessage args)
private void OnMapUidChanged(Entity<HolopadComponent> entity, ref MapUidChangedEvent args)
{
UpdateHolopadControlLockoutStartTime(entity);
UpdateAllUIStates();
}
private void OnPowerChanged(Entity<HolopadComponent> entity, ref PowerChangedEvent args)
{
if (args.Powered)
{
UpdateHolopadControlLockoutStartTime(entity);
}
UpdateAllUIStates();
}
private void OnAnchorChanged(Entity<HolopadComponent> entity, ref AnchorStateChangedEvent args)
{
UpdateAllUIStates();
}
private void OnMobStateChanged(Entity<HolopadUserComponent> ent, ref MobStateChangedEvent args)
@ -472,27 +485,37 @@ public sealed partial class HolopadSystem : SharedHolopadSystem
{
base.Update(frameTime);
_updateTimer += frameTime;
if (_updateTimer >= UpdateTime)
var query = AllEntityQuery<HolopadUserComponent, TransformComponent>();
while (query.MoveNext(out var uid, out var holopadUser, out var xform))
{
_updateTimer -= UpdateTime;
if (HasComp<IgnoreUIRangeComponent>(uid))
continue;
var query = AllEntityQuery<HolopadComponent, TelephoneComponent, TransformComponent>();
while (query.MoveNext(out var uid, out var holopad, out var telephone, out var xform))
foreach (var holopad in holopadUser.LinkedHolopads)
{
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))
if (TryComp<TelephoneComponent>(holopad, out var telephone) &&
!_xformSystem.InRange((holopad.Owner, Transform(holopad)), (uid, xform), telephone.ListeningRange))
{
UnlinkHolopadFromUser((uid, holopad), holopad.User.Value);
UnlinkHolopadFromUser(holopad, (uid, holopadUser));
}
}
}
}
public void UpdateAllUIStates()
{
var querySources = AllEntityQuery<HolopadComponent, TelephoneComponent, UserInterfaceComponent>();
while (querySources.MoveNext(out var uid, out var holopad, out var telephone, out var ui))
{
var uiKey = HasComp<StationAiCoreComponent>(uid) ? HolopadUiKey.AiActionWindow : HolopadUiKey.InteractionWindow;
if (!_userInterfaceSystem.IsUiOpen((uid, ui), uiKey))
continue;
UpdateUIState((uid, holopad), telephone);
}
}
public void UpdateUIState(Entity<HolopadComponent> entity, TelephoneComponent? telephone = null)
{
if (!Resolve(entity.Owner, ref telephone, false))
@ -571,23 +594,26 @@ public sealed partial class HolopadSystem : SharedHolopadSystem
return;
}
if (!TryComp<HolopadUserComponent>(user, out var holopadUser))
holopadUser = AddComp<HolopadUserComponent>(user.Value);
var holopadUser = EnsureComp<HolopadUserComponent>(user.Value);
var userEnt = (user.Value, holopadUser);
if (user != entity.Comp.User?.Owner)
if (user != entity.Comp.User)
{
// Removes the old user from the holopad
UnlinkHolopadFromUser(entity, entity.Comp.User);
if (TryComp<HolopadUserComponent>(entity.Comp.User, out var oldHolopadUser))
{
UnlinkHolopadFromUser(entity, (entity.Comp.User.Value, oldHolopadUser));
}
// Assigns the new user in their place
holopadUser.LinkedHolopads.Add(entity);
entity.Comp.User = (user.Value, holopadUser);
holopadUser?.LinkedHolopads.Add(entity);
entity.Comp.User = user.Value;
}
// Add the new user to PVS and sync their appearance with any
// holopads connected to the one they are using
_pvs.AddGlobalOverride(user.Value);
SyncHolopadHologramAppearanceWithTarget(entity, entity.Comp.User);
SyncHolopadHologramAppearanceWithTarget(entity, userEnt);
}
private void UnlinkHolopadFromUser(Entity<HolopadComponent> entity, Entity<HolopadUserComponent>? user)
@ -611,14 +637,14 @@ public sealed partial class HolopadSystem : SharedHolopadSystem
{
foreach (var linkedHolopad in GetLinkedHolopads(entity))
{
if (linkedHolopad.Comp.Hologram == null)
if (!TryComp<HolopadHologramComponent>(linkedHolopad.Comp.Hologram, out var holopadHologram))
continue;
if (user == null)
_appearanceSystem.SetData(linkedHolopad.Comp.Hologram.Value.Owner, TypingIndicatorVisuals.State, false);
_appearanceSystem.SetData(linkedHolopad.Comp.Hologram.Value, TypingIndicatorVisuals.State, false);
linkedHolopad.Comp.Hologram.Value.Comp.LinkedEntity = user;
Dirty(linkedHolopad.Comp.Hologram.Value);
holopadHologram.LinkedEntity = user;
Dirty(linkedHolopad.Comp.Hologram.Value, holopadHologram);
}
}
@ -626,8 +652,8 @@ public sealed partial class HolopadSystem : SharedHolopadSystem
{
entity.Comp.ControlLockoutOwner = null;
if (entity.Comp.Hologram != null)
DeleteHologram(entity.Comp.Hologram.Value, entity);
if (TryComp<HolopadHologramComponent>(entity.Comp.Hologram, out var holopadHologram))
DeleteHologram((entity.Comp.Hologram.Value, holopadHologram), entity);
// Check if the associated holopad user is an AI
if (HasComp<StationAiHeldComponent>(entity.Comp.User) &&
@ -643,9 +669,9 @@ public sealed partial class HolopadSystem : SharedHolopadSystem
_telephoneSystem.EndTelephoneCalls((stationAiCore.Owner, stationAiCoreTelephone));
}
}
else
else if (TryComp<HolopadUserComponent>(entity.Comp.User, out var holopadUser))
{
UnlinkHolopadFromUser(entity, entity.Comp.User);
UnlinkHolopadFromUser(entity, (entity.Comp.User.Value, holopadUser));
}
Dirty(entity);
@ -749,14 +775,16 @@ public sealed partial class HolopadSystem : SharedHolopadSystem
// Lock out the controls of all involved holopads for a set duration
source.Comp.ControlLockoutOwner = user;
source.Comp.ControlLockoutStartTime = _timing.CurTime;
source.Comp.ControlLockoutEndTime = _timing.CurTime + source.Comp.ControlLockoutDuration;
source.Comp.ControlLockoutCoolDownEndTime = _timing.CurTime + source.Comp.ControlLockoutCoolDown;
Dirty(source);
foreach (var receiver in GetLinkedHolopads(source))
{
receiver.Comp.ControlLockoutOwner = user;
receiver.Comp.ControlLockoutStartTime = _timing.CurTime;
receiver.Comp.ControlLockoutEndTime = _timing.CurTime + source.Comp.ControlLockoutDuration;
receiver.Comp.ControlLockoutCoolDownEndTime = _timing.CurTime + source.Comp.ControlLockoutCoolDown;
Dirty(receiver);
}
@ -796,15 +824,20 @@ public sealed partial class HolopadSystem : SharedHolopadSystem
if (!_telephoneSystem.IsSourceInRangeOfReceiver(sourceTelephoneEntity, receiverTelephoneEntity))
continue;
if (receiverHolopad.ControlLockoutStartTime > source.Comp.ControlLockoutStartTime)
if (receiverHolopad.ControlLockoutEndTime > source.Comp.ControlLockoutEndTime ||
receiverHolopad.ControlLockoutCoolDownEndTime > source.Comp.ControlLockoutCoolDownEndTime)
{
source.Comp.ControlLockoutStartTime = receiverHolopad.ControlLockoutStartTime;
source.Comp.ControlLockoutEndTime = receiverHolopad.ControlLockoutEndTime;
source.Comp.ControlLockoutCoolDownEndTime = receiverHolopad.ControlLockoutCoolDownEndTime;
isDirty = true;
}
}
if (isDirty)
{
Dirty(source);
}
}
private void SetHolopadAmbientState(Entity<HolopadComponent> entity, bool isEnabled)

View file

@ -105,7 +105,9 @@ public sealed partial class TelephoneSystem : SharedTelephoneSystem
RaiseLocalEvent(args.MessageSource, nameEv);
// Determine if speech should be relayed via the telephone itself or a designated speaker
var speaker = entity.Comp.Speaker != null ? entity.Comp.Speaker.Value.Owner : entity.Owner;
var speaker = entity.Comp.Speaker != null
? entity.Comp.Speaker.Value
: entity.Owner;
var name = Loc.GetString("chat-telephone-name-relay",
("originalName", nameEv.VoiceName),
@ -130,8 +132,13 @@ public sealed partial class TelephoneSystem : SharedTelephoneSystem
if (IsTelephoneEngaged(entity))
{
foreach (var receiver in telephone.LinkedTelephones)
foreach (var receiverUid in telephone.LinkedTelephones)
{
if (!TryComp<TelephoneComponent>(receiverUid, out var receiverTelephone))
continue;
var receiver = (receiverUid, receiverTelephone);
if (!IsSourceInRangeOfReceiver(entity, receiver) &&
!IsSourceInRangeOfReceiver(receiver, entity))
{
@ -144,28 +151,28 @@ public sealed partial class TelephoneSystem : SharedTelephoneSystem
{
// Try to play ring tone if ringing
case TelephoneState.Ringing:
if (_timing.CurTime > telephone.StateStartTime + TimeSpan.FromSeconds(telephone.RingingTimeout))
if (_timing.CurTime > telephone.StateStartTime + 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);
telephone.NextRingToneTime = _timing.CurTime + telephone.RingInterval;
}
break;
// Try to hang up if there has been no recent in-call activity
case TelephoneState.InCall:
if (_timing.CurTime > telephone.StateStartTime + TimeSpan.FromSeconds(telephone.IdlingTimeout))
if (_timing.CurTime > telephone.StateStartTime + 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))
if (_timing.CurTime > telephone.StateStartTime + telephone.HangingUpTimeout)
TerminateTelephoneCalls(entity);
break;
@ -180,12 +187,21 @@ public sealed partial class TelephoneSystem : SharedTelephoneSystem
if (IsTelephoneEngaged(source))
return;
// Save the user as the last caller
source.Comp.LastCallerId = GetNameAndJobOfCallingEntity(user);
Dirty(source);
// Attempt to call all receivers
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)
@ -222,12 +238,12 @@ public sealed partial class TelephoneSystem : SharedTelephoneSystem
var callerInfo = GetNameAndJobOfCallingEntity(user);
// Base the name of the device on its label
string? deviceName = null;
if (TryComp<LabelComponent>(source, out var label))
deviceName = label.CurrentLabel;
{
callerInfo.DeviceId = label.CurrentLabel;
}
receiver.Comp.LastCallerId = (callerInfo.Item1, callerInfo.Item2, deviceName); // This will be networked when the state changes
receiver.Comp.LastCallerId = callerInfo; // This will be networked when the state changes
receiver.Comp.LinkedTelephones.Add(source);
receiver.Comp.Muted = options?.MuteReceiver == true;
@ -257,8 +273,12 @@ public sealed partial class TelephoneSystem : SharedTelephoneSystem
if (receiver.Comp.LinkedTelephones.Count != 1)
return;
var source = receiver.Comp.LinkedTelephones.First();
CommenceTelephoneCall(source, receiver);
var sourceUid = receiver.Comp.LinkedTelephones.First();
if (!TryComp<TelephoneComponent>(sourceUid, out var sourceTelephone))
return;
CommenceTelephoneCall((sourceUid, sourceTelephone), receiver);
}
private void CommenceTelephoneCall(Entity<TelephoneComponent> source, Entity<TelephoneComponent> receiver)
@ -311,13 +331,18 @@ public sealed partial class TelephoneSystem : SharedTelephoneSystem
private void HandleEndingTelephoneCalls(Entity<TelephoneComponent> entity, TelephoneState newState)
{
foreach (var linkedTelephone in entity.Comp.LinkedTelephones)
foreach (var linkedUid in entity.Comp.LinkedTelephones)
{
if (!linkedTelephone.Comp.LinkedTelephones.Remove(entity))
if (!TryComp<TelephoneComponent>(linkedUid, out var linkedTelephone))
continue;
if (!IsTelephoneEngaged(linkedTelephone))
EndTelephoneCalls(linkedTelephone);
if (!linkedTelephone.LinkedTelephones.Remove(entity))
continue;
var linked = (linkedUid, linkedTelephone);
if (!IsTelephoneEngaged(linked))
EndTelephoneCalls(linked);
}
entity.Comp.LinkedTelephones.Clear();
@ -372,10 +397,13 @@ public sealed partial class TelephoneSystem : SharedTelephoneSystem
var evReceivedMessage = new TelephoneMessageReceivedEvent(message, chatMsg, messageSource, source);
foreach (var receiver in source.Comp.LinkedTelephones)
foreach (var receiverUid in source.Comp.LinkedTelephones)
{
RaiseLocalEvent(receiver, ref evReceivedMessage);
receiver.Comp.StateStartTime = _timing.CurTime;
if (!TryComp<TelephoneComponent>(receiverUid, out var receiverTelephone))
continue;
RaiseLocalEvent(receiverUid, ref evReceivedMessage);
receiverTelephone.StateStartTime = _timing.CurTime;
}
if (name != Name(messageSource))
@ -390,6 +418,7 @@ public sealed partial class TelephoneSystem : SharedTelephoneSystem
{
var oldState = entity.Comp.CurrentState;
entity.Comp.PreviousState = entity.Comp.CurrentState;
entity.Comp.CurrentState = newState;
entity.Comp.StateStartTime = _timing.CurTime;
Dirty(entity);
@ -419,24 +448,21 @@ public sealed partial class TelephoneSystem : SharedTelephoneSystem
entity.Comp.Speaker = speaker;
}
private (string?, string?) GetNameAndJobOfCallingEntity(EntityUid uid)
private TelephoneCallRecord GetNameAndJobOfCallingEntity(EntityUid uid)
{
string? presumedName = null;
string? presumedJob = null;
var record = new TelephoneCallRecord();
if (HasComp<StationAiHeldComponent>(uid) || HasComp<BorgChassisComponent>(uid))
{
presumedName = Name(uid);
return (presumedName, presumedJob);
record.CallerId = Name(uid);
}
if (_idCardSystem.TryFindIdCard(uid, out var idCard))
else if (_idCardSystem.TryFindIdCard(uid, out var idCard))
{
presumedName = string.IsNullOrWhiteSpace(idCard.Comp.FullName) ? null : idCard.Comp.FullName;
presumedJob = idCard.Comp.LocalizedJobTitle;
record.CallerId = string.IsNullOrWhiteSpace(idCard.Comp.FullName) ? null : idCard.Comp.FullName;
record.CallerJob = idCard.Comp.LocalizedJobTitle;
}
return (presumedName, presumedJob);
return record;
}
public bool IsSourceAbleToReachReceiver(Entity<TelephoneComponent> source, Entity<TelephoneComponent> receiver)

View file

@ -11,21 +11,21 @@ namespace Content.Shared.Holopad;
/// <remarks>
/// Holopads also require a <see cref="TelephoneComponent"/> to function
/// </remarks>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, AutoGenerateComponentPause]
[Access(typeof(SharedHolopadSystem))]
public sealed partial class HolopadComponent : Component
{
/// <summary>
/// The entity being projected by the holopad
/// </summary>
[ViewVariables]
public Entity<HolopadHologramComponent>? Hologram;
[DataField]
public EntityUid? Hologram;
/// <summary>
/// The entity using the holopad
/// </summary>
[ViewVariables]
public Entity<HolopadUserComponent>? User;
[DataField]
public EntityUid? User;
/// <summary>
/// Proto ID for the user's hologram
@ -36,26 +36,32 @@ public sealed partial class HolopadComponent : Component
/// <summary>
/// The entity that has locked out the controls of this device
/// </summary>
[ViewVariables, AutoNetworkedField]
[DataField, AutoNetworkedField]
public EntityUid? ControlLockoutOwner = null;
/// <summary>
/// The game tick the control lockout was initiated
/// The time the control lockout will end
/// </summary>
[ViewVariables, AutoNetworkedField]
public TimeSpan ControlLockoutStartTime;
[DataField, AutoNetworkedField, AutoPausedField]
public TimeSpan ControlLockoutEndTime;
/// <summary>
/// The time the control lockout cool down will end
/// </summary>
[DataField, AutoNetworkedField, AutoPausedField]
public TimeSpan ControlLockoutCoolDownEndTime;
/// <summary>
/// The duration that the control lockout will last in seconds
/// </summary>
[DataField]
public float ControlLockoutDuration { get; private set; } = 90f;
public TimeSpan ControlLockoutDuration { get; private set; } = TimeSpan.FromSeconds(90);
/// <summary>
/// The duration before the controls can be lockout again in seconds
/// </summary>
[DataField]
public float ControlLockoutCoolDown { get; private set; } = 180f;
public TimeSpan ControlLockoutCoolDown { get; private set; } = TimeSpan.FromSeconds(180);
}
#region: Event messages

View file

@ -8,10 +8,7 @@ public abstract partial class SharedHolopadSystem : EntitySystem
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)
if (_timing.CurTime > entity.Comp.ControlLockoutEndTime)
return false;
if (entity.Comp.ControlLockoutOwner == null || entity.Comp.ControlLockoutOwner == user)
@ -22,15 +19,12 @@ public abstract partial class SharedHolopadSystem : EntitySystem
public TimeSpan GetHolopadControlLockedPeriod(Entity<HolopadComponent> entity)
{
return entity.Comp.ControlLockoutStartTime + TimeSpan.FromSeconds(entity.Comp.ControlLockoutDuration) - _timing.CurTime;
return entity.Comp.ControlLockoutEndTime - _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)
if (_timing.CurTime > entity.Comp.ControlLockoutCoolDownEndTime)
return false;
return true;
@ -38,6 +32,6 @@ public abstract partial class SharedHolopadSystem : EntitySystem
public TimeSpan GetHolopadBroadcastCoolDown(Entity<HolopadComponent> entity)
{
return entity.Comp.ControlLockoutStartTime + TimeSpan.FromSeconds(entity.Comp.ControlLockoutCoolDown) - _timing.CurTime;
return entity.Comp.ControlLockoutCoolDownEndTime - _timing.CurTime;
}
}

View file

@ -1,3 +1,4 @@
using Robust.Shared.Utility;
using System.Linq;
namespace Content.Shared.Telephone;
@ -21,15 +22,15 @@ public abstract class SharedTelephoneSystem : EntitySystem
if (presumedJob != null)
callerId = Loc.GetString("chat-telephone-caller-id-with-job",
("callerName", presumedName),
("callerJob", presumedJob),
("callerName", FormattedMessage.RemoveMarkupPermissive(presumedName)),
("callerJob", FormattedMessage.RemoveMarkupPermissive(presumedJob)),
("color", fontColor),
("fontType", fontType),
("fontSize", fontSize));
else
callerId = Loc.GetString("chat-telephone-caller-id-without-job",
("callerName", presumedName),
("callerName", FormattedMessage.RemoveMarkupPermissive(presumedName)),
("color", fontColor),
("fontType", fontType),
("fontSize", fontSize));
@ -48,7 +49,7 @@ public abstract class SharedTelephoneSystem : EntitySystem
}
return Loc.GetString("chat-telephone-device-id",
("deviceName", deviceName),
("deviceName", FormattedMessage.RemoveMarkupPermissive(deviceName)),
("color", fontColor),
("fontType", fontType),
("fontSize", fontSize));

View file

@ -1,12 +1,11 @@
using Content.Shared.Chat;
using Content.Shared.Speech;
using Robust.Shared.Audio;
using Robust.Shared.GameStates;
using Robust.Shared.Serialization;
namespace Content.Shared.Telephone;
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, AutoGenerateComponentPause]
[Access(typeof(SharedTelephoneSystem))]
public sealed partial class TelephoneComponent : Component
{
@ -14,19 +13,19 @@ public sealed partial class TelephoneComponent : Component
/// Sets how long the telephone will ring before it automatically hangs up
/// </summary>
[DataField]
public float RingingTimeout = 30;
public TimeSpan RingingTimeout = TimeSpan.FromSeconds(30);
/// <summary>
/// Sets how long the telephone can remain idle in-call before it automatically hangs up
/// </summary>
[DataField]
public float IdlingTimeout = 60;
public TimeSpan IdlingTimeout = TimeSpan.FromSeconds(60);
/// <summary>
/// Sets how long the telephone will stay in the hanging up state before return to idle
/// </summary>
[DataField]
public float HangingUpTimeout = 2;
public TimeSpan HangingUpTimeout = TimeSpan.FromSeconds(2);
/// <summary>
/// Tone played while the phone is ringing
@ -38,7 +37,7 @@ public sealed partial class TelephoneComponent : Component
/// Sets the number of seconds before the next ring tone is played
/// </summary>
[DataField]
public float RingInterval = 2f;
public TimeSpan RingInterval = TimeSpan.FromSeconds(2);
/// <summary>
/// The time at which the next tone will be played
@ -90,8 +89,8 @@ public sealed partial class TelephoneComponent : Component
/// <summary>
/// Speech is relayed through this entity instead of the telephone
/// </summary>
[ViewVariables(VVAccess.ReadOnly)]
public Entity<SpeechComponent>? Speaker = null;
[DataField]
public EntityUid? Speaker = null;
/// <summary>
/// Telephone number for this device
@ -99,41 +98,56 @@ public sealed partial class TelephoneComponent : Component
/// <remarks>
/// For future use - a system for generating and handling telephone numbers has not been implemented yet
/// </remarks>
[ViewVariables]
[DataField]
public int TelephoneNumber = -1;
/// <summary>
/// Linked telephone
/// Other telephones that have been linked to this one
/// </summary>
[ViewVariables]
public HashSet<Entity<TelephoneComponent>> LinkedTelephones = new();
[DataField]
public HashSet<EntityUid> LinkedTelephones = new();
/// <summary>
/// Defines the current state the telephone is in
/// </summary>
[ViewVariables, AutoNetworkedField]
[DataField, AutoNetworkedField]
public TelephoneState CurrentState = TelephoneState.Idle;
/// <summary>
/// Defines the previous state the telephone was in
/// </summary>
[DataField, AutoNetworkedField]
public TelephoneState PreviousState = TelephoneState.Idle;
/// <summary>
/// The game tick the current state started
/// </summary>
[ViewVariables]
[DataField, AutoPausedField]
public TimeSpan StateStartTime;
/// <summary>
/// Sets whether the telphone can pick up nearby speech
/// </summary>
[ViewVariables]
[DataField]
public bool Muted = false;
/// <summary>
/// The presumed name and/or job of the last person to call this telephone
/// and the name of the device that they used to do so
/// </summary>
[ViewVariables, AutoNetworkedField]
public (string?, string?, string?) LastCallerId;
[DataField, AutoNetworkedField]
public TelephoneCallRecord? LastCallerId;
}
/// <summary>
/// A telephone call record.
/// </summary>
/// <param name="CallerId">The name of the person who placed the call.</param>
/// <param name="CallerJob">The job of the person who placed the call.</param>
/// <param name="DeviceId">The name of the device used to make the call.</param>
[Serializable, NetSerializable]
public record struct TelephoneCallRecord(string? CallerId, string? CallerJob, string? DeviceId);
#region: Telephone events
/// <summary>