Activatable UI component (#5184)

* Transfer most Instrument UI logic to a new component, ActivatableUIComponent

* Move more ActivatableUIComponent stuff to ECS

* ActivatableUI component ignore on client

* ActivatableUI: Get rid of component interfaces where possible

* Add in adminOnly attribute for activatable UIs

This is so that porting #4926 to this will be easier

* Transition Solar Control Computer to ActivatableUI

* Move communications console to ActivatableUI

* Move cargo console to ActivatableUI

* Move ID card console to ActivatableUI

* ActivatableUI: Make things more amiable to entity tests adding components weirdly

* ActivatableUI: Use handling or lack thereof of events properly

* ActivatableUI: component dependency issue resolution stuffs

* ActivatableUISystem: Fix #5258

* More fixes because master did stuffo

* Check for HandDeselectedEvent again because otherwise active-hand check doesn't work

* Move just a bit more code into the system, introduce a workaround for #5258

* Purge the player status detection stuff

* Oh and some obsolete stuff too
This commit is contained in:
20kdc
2021-11-23 18:19:08 +00:00
committed by GitHub
parent b063209f99
commit f6d44be34f
20 changed files with 366 additions and 413 deletions

View File

@@ -216,6 +216,8 @@ namespace Content.Client.Entry
"Log",
"Hoe",
"Seed",
"ActivatableUI",
"ActivatableUIRequiresPower",
"BotanySharp",
"PlantSampleTaker",
"Internals",

View File

@@ -21,8 +21,7 @@ using Robust.Shared.ViewVariables;
namespace Content.Server.Access.Components
{
[RegisterComponent]
[ComponentReference(typeof(IActivate))]
public class IdCardConsoleComponent : SharedIdCardConsoleComponent, IActivate, IInteractUsing, IBreakAct
public class IdCardConsoleComponent : SharedIdCardConsoleComponent, IInteractUsing, IBreakAct
{
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
@@ -207,17 +206,6 @@ namespace Content.Server.Access.Components
UserInterface?.SetState(newState);
}
void IActivate.Activate(ActivateEventArgs eventArgs)
{
if (!eventArgs.User.TryGetComponent(out ActorComponent? actor))
{
return;
}
if (!Powered) return;
UserInterface?.Open(actor.PlayerSession);
}
async Task<bool> IInteractUsing.InteractUsing(InteractUsingEventArgs eventArgs)
{
var item = eventArgs.Using;

View File

@@ -1,142 +0,0 @@
using System;
using Content.Server.Power.Components;
using Content.Server.UserInterface;
using Content.Shared.ActionBlocker;
using Content.Shared.Interaction;
using Content.Shared.Popups;
using Robust.Server.GameObjects;
using Robust.Shared.GameObjects;
using Robust.Shared.Localization;
using Robust.Shared.ViewVariables;
namespace Content.Server.GameObjects.Components
{
/// <summary>
/// This component is used as a base class for classes like SolarControlConsoleComponent.
/// These components operate the server-side logic for the "primary UI" of a computer.
/// That means showing the UI when a user activates it, for example.
/// </summary>
public abstract class BaseComputerUserInterfaceComponent : Component
{
protected readonly object UserInterfaceKey;
[ViewVariables] protected BoundUserInterface? UserInterface => Owner.GetUIOrNull(UserInterfaceKey);
[ViewVariables] public bool Powered => !Owner.TryGetComponent(out ApcPowerReceiverComponent? receiver) || receiver.Powered;
public BaseComputerUserInterfaceComponent(object key)
{
UserInterfaceKey = key;
}
protected override void Initialize()
{
base.Initialize();
if (UserInterface != null)
UserInterface.OnReceiveMessage += OnReceiveUIMessageCallback;
}
/// <summary>
/// Internal callback used to grab session and session attached entity before any more work is done.
/// This is so that sessionEntity is always available to checks up and down the line.
/// </summary>
private void OnReceiveUIMessageCallback(ServerBoundUserInterfaceMessage obj)
{
var session = obj.Session;
var sessionEntity = session.AttachedEntity;
if (sessionEntity == null)
return; // No session entity, so we're probably not able to touch this.
OnReceiveUnfilteredUserInterfaceMessage(obj, sessionEntity);
}
/// <summary>
/// Override this to handle messages from the UI before filtering them.
/// Calling base is necessary if you want this class to have any meaning.
/// </summary>
protected void OnReceiveUnfilteredUserInterfaceMessage(ServerBoundUserInterfaceMessage obj, IEntity sessionEntity)
{
// "Across all computers" "anti-cheats" ought to be put here or at some parent level (BaseDeviceUserInterfaceComponent?)
// Determine some facts about the session.
// Powered?
if (!Powered)
{
sessionEntity.PopupMessageCursor(Loc.GetString("base-computer-ui-component-not-powered"));
return; // Not powered, so this computer should probably do nothing.
}
// Can we interact?
if (!EntitySystem.Get<ActionBlockerSystem>().CanInteract(sessionEntity.Uid))
{
sessionEntity.PopupMessageCursor(Loc.GetString("base-computer-ui-component-cannot-interact"));
return;
}
// Good to go!
OnReceiveUserInterfaceMessage(obj);
}
/// <summary>
/// Override this to handle messages from the UI.
/// Calling base is unnecessary.
/// These messages will automatically be blocked if the user shouldn't be able to access this computer, or if the computer has lost power.
/// </summary>
protected virtual void OnReceiveUserInterfaceMessage(ServerBoundUserInterfaceMessage obj)
{
// Nothing!
}
[Obsolete("Component Messages are deprecated, use Entity Events instead.")]
public override void HandleMessage(ComponentMessage message, IComponent? component)
{
#pragma warning disable 618
base.HandleMessage(message, component);
#pragma warning restore 618
switch (message)
{
case PowerChangedMessage powerChanged:
PowerReceiverOnOnPowerStateChanged(powerChanged);
break;
}
}
private void PowerReceiverOnOnPowerStateChanged(PowerChangedMessage e)
{
if (!e.Powered)
{
// We need to kick off users who are using it when it loses power.
UserInterface?.CloseAll();
// Now alert subclass.
ComputerLostPower();
}
}
/// <summary>
/// Override this if you want the computer to do something when it loses power (i.e. reset state)
/// All UIs should have been closed by the time this is called.
/// Calling base is unnecessary.
/// </summary>
public virtual void ComputerLostPower()
{
}
/// <summary>
/// This is called from ComputerUIActivatorSystem.
/// Override this to add additional activation conditions of some sort.
/// Calling base runs standard activation logic.
/// *This remains inside the component for overridability.*
/// </summary>
public virtual void ActivateThunk(ActivateInWorldEvent eventArgs)
{
if (!eventArgs.User.TryGetComponent(out ActorComponent? actor))
{
return;
}
if (!Powered)
{
Owner.PopupMessage(eventArgs.User, Loc.GetString("base-computer-ui-component-not-powered"));
return;
}
UserInterface?.Open(actor.PlayerSession);
}
}
}

View File

@@ -19,8 +19,7 @@ using Robust.Shared.ViewVariables;
namespace Content.Server.Cargo.Components
{
[RegisterComponent]
[ComponentReference(typeof(IActivate))]
public class CargoConsoleComponent : SharedCargoConsoleComponent, IActivate
public class CargoConsoleComponent : SharedCargoConsoleComponent
{
[Dependency] private readonly IMapManager _mapManager = default!;
@@ -203,18 +202,6 @@ namespace Content.Server.Cargo.Components
}
}
void IActivate.Activate(ActivateEventArgs eventArgs)
{
if (!eventArgs.User.TryGetComponent(out ActorComponent? actor))
{
return;
}
if (!Powered)
return;
UserInterface?.Open(actor.PlayerSession);
}
private void UpdateUIState()
{
if (_bankAccount == null || !Owner.IsValid())

View File

@@ -1,7 +1,6 @@
using System.Linq;
using Content.Server.Cleanable;
using Content.Server.Coordinates.Helpers;
using Content.Server.GameObjects.Components;
using Content.Shared.Chemistry;
using Content.Shared.Chemistry.Reaction;
using Content.Shared.Chemistry.Reagent;

View File

@@ -21,8 +21,7 @@ using Timer = Robust.Shared.Timing.Timer;
namespace Content.Server.Communications
{
[RegisterComponent]
[ComponentReference(typeof(IActivate))]
public class CommunicationsConsoleComponent : SharedCommunicationsConsoleComponent, IActivate
public class CommunicationsConsoleComponent : SharedCommunicationsConsoleComponent
{
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly IChatManager _chatManager = default!;
@@ -121,23 +120,5 @@ namespace Content.Server.Communications
break;
}
}
public void OpenUserInterface(IPlayerSession session)
{
UserInterface?.Open(session);
}
void IActivate.Activate(ActivateEventArgs eventArgs)
{
if (!eventArgs.User.TryGetComponent(out ActorComponent? actor))
return;
/*
if (!Powered)
{
return;
}
*/
OpenUserInterface(actor.PlayerSession);
}
}
}

View File

@@ -1,22 +0,0 @@
using Content.Server.GameObjects.Components;
using Content.Shared.Interaction;
using JetBrains.Annotations;
using Robust.Shared.GameObjects;
namespace Content.Server.Computer
{
[UsedImplicitly]
internal sealed class ComputerUIActivatorSystem : EntitySystem
{
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<BaseComputerUserInterfaceComponent, ActivateInWorldEvent>(HandleActivate);
}
private void HandleActivate(EntityUid uid, BaseComputerUserInterfaceComponent component, ActivateInWorldEvent args)
{
component.ActivateThunk(args);
}
}
}

View File

@@ -1,6 +1,5 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Content.Server.GameObjects.Components;
using Content.Server.WireHacking;
using Content.Shared.Construction;
using Content.Shared.Examine;

View File

@@ -25,27 +25,11 @@ namespace Content.Server.Instruments
{
[RegisterComponent]
[ComponentReference(typeof(IActivate))]
public class InstrumentComponent
: SharedInstrumentComponent,
IDropped,
IHandSelected,
IHandDeselected,
IActivate,
IUse,
IThrown
: SharedInstrumentComponent
{
private InstrumentSystem _instrumentSystem = default!;
/// <summary>
/// The client channel currently playing the instrument, or null if there's none.
/// </summary>
[ViewVariables]
private IPlayerSession? _instrumentPlayer;
[DataField("handheld")]
private bool _handheld;
[ViewVariables]
private bool _playing = false;
@@ -115,11 +99,7 @@ namespace Content.Server.Instruments
}
}
/// <summary>
/// Whether the instrument is an item which can be held or not.
/// </summary>
[ViewVariables]
public bool Handheld => _handheld;
public IPlayerSession? InstrumentPlayer => Owner.GetComponentOrNull<ActivatableUIComponent>()?.CurrentSingleUser;
/// <summary>
/// Whether the instrument is currently playing or not.
@@ -135,41 +115,12 @@ namespace Content.Server.Instruments
}
}
public IPlayerSession? InstrumentPlayer
{
get => _instrumentPlayer;
private set
{
Playing = false;
if (_instrumentPlayer != null)
_instrumentPlayer.PlayerStatusChanged -= OnPlayerStatusChanged;
_instrumentPlayer = value;
if (value != null)
_instrumentPlayer!.PlayerStatusChanged += OnPlayerStatusChanged;
}
}
[ViewVariables] private BoundUserInterface? UserInterface => Owner.GetUIOrNull(InstrumentUiKey.Key);
private void OnPlayerStatusChanged(object? sender, SessionStatusEventArgs e)
{
if (e.Session != _instrumentPlayer || e.NewStatus != SessionStatus.Disconnected) return;
InstrumentPlayer = null;
Clean();
}
protected override void Initialize()
{
base.Initialize();
if (UserInterface != null)
{
UserInterface.OnClosed += UserInterfaceOnClosed;
}
_instrumentSystem = EntitySystem.Get<InstrumentSystem>();
}
@@ -190,7 +141,7 @@ namespace Content.Server.Instruments
switch (message)
{
case InstrumentMidiEventMessage midiEventMsg:
if (!Playing || session != _instrumentPlayer || InstrumentPlayer == null) return;
if (!Playing || session != InstrumentPlayer || InstrumentPlayer == null) return;
var send = true;
@@ -237,12 +188,12 @@ namespace Content.Server.Instruments
_lastSequencerTick = Math.Max(maxTick, minTick);
break;
case InstrumentStartMidiMessage startMidi:
if (session != _instrumentPlayer)
if (session != InstrumentPlayer)
break;
Playing = true;
break;
case InstrumentStopMidiMessage stopMidi:
if (session != _instrumentPlayer)
if (session != InstrumentPlayer)
break;
Playing = false;
Clean();
@@ -250,99 +201,20 @@ namespace Content.Server.Instruments
}
}
private void Clean()
public void Clean()
{
if (Playing)
{
#pragma warning disable 618
SendNetworkMessage(new InstrumentStopMidiMessage());
#pragma warning restore 618
}
Playing = false;
_lastSequencerTick = 0;
_batchesDropped = 0;
_laggedBatches = 0;
}
void IDropped.Dropped(DroppedEventArgs eventArgs)
{
Clean();
#pragma warning disable 618
SendNetworkMessage(new InstrumentStopMidiMessage());
#pragma warning restore 618
InstrumentPlayer = null;
UserInterface?.CloseAll();
}
void IThrown.Thrown(ThrownEventArgs eventArgs)
{
Clean();
#pragma warning disable 618
SendNetworkMessage(new InstrumentStopMidiMessage());
#pragma warning restore 618
InstrumentPlayer = null;
UserInterface?.CloseAll();
}
void IHandSelected.HandSelected(HandSelectedEventArgs eventArgs)
{
if (eventArgs.User == null || !eventArgs.User.TryGetComponent(out ActorComponent? actor))
return;
var session = actor.PlayerSession;
if (session.Status != SessionStatus.InGame) return;
InstrumentPlayer = session;
}
void IHandDeselected.HandDeselected(HandDeselectedEventArgs eventArgs)
{
Clean();
#pragma warning disable 618
SendNetworkMessage(new InstrumentStopMidiMessage());
#pragma warning restore 618
UserInterface?.CloseAll();
}
void IActivate.Activate(ActivateEventArgs eventArgs)
{
if (Handheld)
return;
InteractInstrument(eventArgs.User);
}
bool IUse.UseEntity(UseEntityEventArgs eventArgs)
{
InteractInstrument(eventArgs.User);
return false;
}
private void InteractInstrument(IEntity user)
{
if (!user.TryGetComponent(out ActorComponent? actor)) return;
if ((!Handheld && InstrumentPlayer != null)
|| (Handheld && actor.PlayerSession != InstrumentPlayer)
|| !EntitySystem.Get<ActionBlockerSystem>().CanInteract(user.Uid)) return;
InstrumentPlayer = actor.PlayerSession;
OpenUserInterface(InstrumentPlayer);
return;
}
private void UserInterfaceOnClosed(IPlayerSession player)
{
if (Handheld || player != InstrumentPlayer) return;
Clean();
InstrumentPlayer = null;
#pragma warning disable 618
SendNetworkMessage(new InstrumentStopMidiMessage());
#pragma warning restore 618
}
private void OpenUserInterface(IPlayerSession session)
{
UserInterface?.Toggle(session);
}
public override void Update(float delta)
{
base.Update(delta);
@@ -350,37 +222,22 @@ namespace Content.Server.Instruments
var maxMidiLaggedBatches = _instrumentSystem.MaxMidiLaggedBatches;
var maxMidiBatchDropped = _instrumentSystem.MaxMidiBatchesDropped;
if (_instrumentPlayer != null
&& (_instrumentPlayer.AttachedEntityUid == null
|| !EntitySystem.Get<ActionBlockerSystem>().CanInteract(_instrumentPlayer.AttachedEntityUid.Value)))
{
InstrumentPlayer = null;
Clean();
UserInterface?.CloseAll();
}
if ((_batchesDropped >= maxMidiBatchDropped
|| _laggedBatches >= maxMidiLaggedBatches)
&& InstrumentPlayer != null && _respectMidiLimits)
{
var mob = InstrumentPlayer.AttachedEntity;
#pragma warning disable 618
SendNetworkMessage(new InstrumentStopMidiMessage());
#pragma warning restore 618
Playing = false;
// Just in case
Clean();
UserInterface?.CloseAll();
if (mob != null)
{
EntitySystem.Get<StunSystem>().TryParalyze(mob.Uid, TimeSpan.FromSeconds(1));
Clean();
Owner.PopupMessage(mob, "instrument-component-finger-cramps-max-message");
}
InstrumentPlayer = null;
}
_timer += delta;

View File

@@ -1,5 +1,6 @@
using Content.Shared;
using Content.Shared.CCVar;
using Content.Server.UserInterface;
using JetBrains.Annotations;
using Robust.Shared.Configuration;
using Robust.Shared.GameObjects;
@@ -20,6 +21,8 @@ namespace Content.Server.Instruments
_cfg.OnValueChanged(CCVars.MaxMidiEventsPerBatch, OnMaxMidiEventsPerBatchChanged, true);
_cfg.OnValueChanged(CCVars.MaxMidiBatchesDropped, OnMaxMidiBatchesDroppedChanged, true);
_cfg.OnValueChanged(CCVars.MaxMidiLaggedBatches, OnMaxMidiLaggedBatchesChanged, true);
SubscribeLocalEvent<InstrumentComponent, ActivatableUIPlayerChangedEvent>(InstrumentNeedsClean);
}
public int MaxMidiEventsPerSecond { get; private set; }
@@ -47,6 +50,11 @@ namespace Content.Server.Instruments
MaxMidiEventsPerSecond = obj;
}
private void InstrumentNeedsClean(EntityUid uid, InstrumentComponent component, ActivatableUIPlayerChangedEvent ev)
{
component.Clean();
}
public override void Update(float frameTime)
{
base.Update(frameTime);

View File

@@ -1,6 +1,5 @@
using System;
using Content.Server.Containers;
using Content.Server.GameObjects;
using Content.Server.Objectives.Interfaces;
using JetBrains.Annotations;
using Robust.Shared.Containers;

View File

@@ -0,0 +1,12 @@
using System;
using Robust.Shared.GameObjects;
namespace Content.Server.Power.Components
{
[RegisterComponent]
public class ActivatableUIRequiresPowerComponent : Component
{
public override string Name => "ActivatableUIRequiresPower";
}
}

View File

@@ -0,0 +1,53 @@
using System.Linq;
using Content.Shared;
using Content.Shared.CCVar;
using Content.Shared.ActionBlocker;
using Content.Shared.Hands;
using Content.Shared.Popups;
using Content.Shared.Standing;
using Content.Shared.Stunnable;
using Content.Shared.Throwing;
using Content.Shared.Interaction;
using Content.Shared.Interaction.Helpers;
using Content.Server.Power.Components;
using Content.Server.UserInterface;
using JetBrains.Annotations;
using Robust.Server.GameObjects;
using Robust.Server.Player;
using Robust.Shared.Configuration;
using Robust.Shared.Localization;
using Robust.Shared.GameObjects;
using Robust.Shared.Network;
using Robust.Shared.IoC;
namespace Content.Server.Power.EntitySystems
{
[UsedImplicitly]
internal sealed class ActivatableUIRequiresPowerSystem : EntitySystem
{
[Dependency] private readonly ActivatableUISystem _activatableUISystem = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<ActivatableUIRequiresPowerComponent, ActivatableUIOpenAttemptEvent>(OnActivate);
SubscribeLocalEvent<ActivatableUIRequiresPowerComponent, PowerChangedEvent>(OnPowerChanged);
}
private void OnActivate(EntityUid uid, ActivatableUIRequiresPowerComponent component, ActivatableUIOpenAttemptEvent args)
{
if (args.Cancelled) return;
if (EntityManager.TryGetComponent<ApcPowerReceiverComponent>(uid, out var power) && !power.Powered)
{
args.User.PopupMessageCursor(Loc.GetString("base-computer-ui-component-not-powered"));
args.Cancel();
}
}
private void OnPowerChanged(EntityUid uid, ActivatableUIRequiresPowerComponent component, PowerChangedEvent args)
{
if (!args.Powered)
_activatableUISystem.CloseAll(uid);
}
}
}

View File

@@ -1,7 +1,7 @@
using System;
using Content.Shared.Solar;
using Content.Server.Solar.EntitySystems;
using Content.Server.GameObjects.Components;
using Content.Server.UserInterface;
using Robust.Server.GameObjects;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
@@ -11,45 +11,8 @@ using Robust.Shared.Maths;
namespace Content.Server.Solar.Components
{
[RegisterComponent]
[ComponentReference(typeof(BaseComputerUserInterfaceComponent))]
public class SolarControlConsoleComponent : BaseComputerUserInterfaceComponent
public class SolarControlConsoleComponent : Component
{
public override string Name => "SolarControlConsole";
[Dependency] private readonly IEntitySystemManager _entitySystemManager = default!;
private PowerSolarSystem _powerSolarSystem = default!;
public SolarControlConsoleComponent() : base(SolarControlConsoleUiKey.Key) { }
protected override void Initialize()
{
base.Initialize();
_powerSolarSystem = _entitySystemManager.GetEntitySystem<PowerSolarSystem>();
}
public void UpdateUIState()
{
UserInterface?.SetState(new SolarControlConsoleBoundInterfaceState(_powerSolarSystem.TargetPanelRotation, _powerSolarSystem.TargetPanelVelocity, _powerSolarSystem.TotalPanelPower, _powerSolarSystem.TowardsSun));
}
protected override void OnReceiveUserInterfaceMessage(ServerBoundUserInterfaceMessage obj)
{
switch (obj.Message)
{
case SolarControlConsoleAdjustMessage msg:
if (double.IsFinite(msg.Rotation))
{
_powerSolarSystem.TargetPanelRotation = msg.Rotation.Reduced();
}
if (double.IsFinite(msg.AngularVelocity))
{
var degrees = msg.AngularVelocity.Degrees;
degrees = Math.Clamp(degrees, -PowerSolarSystem.MaxPanelVelocityDegrees, PowerSolarSystem.MaxPanelVelocityDegrees);
_powerSolarSystem.TargetPanelVelocity = Angle.FromDegrees(degrees);
}
break;
}
}
}
}

View File

@@ -1,6 +1,12 @@
using System;
using Content.Server.Solar.Components;
using Content.Server.UserInterface;
using Content.Shared.Solar;
using JetBrains.Annotations;
using Robust.Shared.GameObjects;
using Robust.Server.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Maths;
namespace Content.Server.Solar.EntitySystems
{
@@ -10,22 +16,53 @@ namespace Content.Server.Solar.EntitySystems
[UsedImplicitly]
internal sealed class PowerSolarControlConsoleSystem : EntitySystem
{
[Dependency] private PowerSolarSystem _powerSolarSystem = default!;
/// <summary>
/// Timer used to avoid updating the UI state every frame (which would be overkill)
/// </summary>
private float _updateTimer;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<SolarControlConsoleComponent, ServerBoundUserInterfaceMessage>(OnUIMessage);
}
public override void Update(float frameTime)
{
_updateTimer += frameTime;
if (_updateTimer >= 1)
{
_updateTimer -= 1;
var state = new SolarControlConsoleBoundInterfaceState(_powerSolarSystem.TargetPanelRotation, _powerSolarSystem.TargetPanelVelocity, _powerSolarSystem.TotalPanelPower, _powerSolarSystem.TowardsSun);
foreach (var component in EntityManager.EntityQuery<SolarControlConsoleComponent>())
{
component.UpdateUIState();
component.Owner.GetUIOrNull(SolarControlConsoleUiKey.Key)?.SetState(state);
}
}
}
private void OnUIMessage(EntityUid uid, SolarControlConsoleComponent component, ServerBoundUserInterfaceMessage obj)
{
if (component.Deleted) return;
switch (obj.Message)
{
case SolarControlConsoleAdjustMessage msg:
if (double.IsFinite(msg.Rotation))
{
_powerSolarSystem.TargetPanelRotation = msg.Rotation.Reduced();
}
if (double.IsFinite(msg.AngularVelocity))
{
var degrees = msg.AngularVelocity.Degrees;
degrees = Math.Clamp(degrees, -PowerSolarSystem.MaxPanelVelocityDegrees, PowerSolarSystem.MaxPanelVelocityDegrees);
_powerSolarSystem.TargetPanelVelocity = Angle.FromDegrees(degrees);
}
break;
}
}
}
}

View File

@@ -0,0 +1,60 @@
using System;
using Content.Shared.Instruments;
using Content.Shared.Interaction;
using Robust.Server.GameObjects;
using Robust.Server.Player;
using Robust.Shared.Reflection;
using Robust.Shared.GameObjects;
using Robust.Shared.Enums;
using Robust.Shared.Player;
using Robust.Shared.Network;
using Robust.Shared.IoC;
using Robust.Shared.Utility;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.ViewVariables;
namespace Content.Server.UserInterface
{
[RegisterComponent]
public class ActivatableUIComponent : Component,
ISerializationHooks
{
public override string Name => "ActivatableUI";
[ViewVariables]
public Enum? Key { get; set; }
[ViewVariables] public BoundUserInterface? UserInterface => (Key != null) ? Owner.GetUIOrNull(Key) : null;
[ViewVariables(VVAccess.ReadWrite)]
[DataField("inHandsOnly")]
public bool InHandsOnly { get; set; } = false;
[ViewVariables]
[DataField("singleUser")]
public bool SingleUser { get; set; } = false;
[ViewVariables(VVAccess.ReadWrite)]
[DataField("adminOnly")]
public bool AdminOnly { get; set; } = false;
[DataField("key", readOnly: true, required: true)]
private string _keyRaw = default!;
/// <summary>
/// The client channel currently using the object, or null if there's none/not single user.
/// NOTE: DO NOT DIRECTLY SET, USE ActivatableUISystem.SetCurrentSingleUser
/// </summary>
[ViewVariables]
public IPlayerSession? CurrentSingleUser;
void ISerializationHooks.AfterDeserialization()
{
var reflectionManager = IoCManager.Resolve<IReflectionManager>();
if (reflectionManager.TryParseEnumReference(_keyRaw, out var key))
Key = key;
}
}
}

View File

@@ -0,0 +1,154 @@
using System.Linq;
using Content.Shared;
using Content.Shared.CCVar;
using Content.Shared.ActionBlocker;
using Content.Shared.Hands;
using Content.Shared.Popups;
using Content.Shared.Standing;
using Content.Shared.Stunnable;
using Content.Shared.Throwing;
using Content.Shared.Interaction;
using Content.Shared.Interaction.Helpers;
using Content.Server.Administration.Managers;
using JetBrains.Annotations;
using Robust.Server.GameObjects;
using Robust.Server.Player;
using Robust.Shared.Configuration;
using Robust.Shared.Localization;
using Robust.Shared.GameObjects;
using Robust.Shared.Network;
using Robust.Shared.IoC;
namespace Content.Server.UserInterface
{
[UsedImplicitly]
internal sealed class ActivatableUISystem : EntitySystem
{
[Dependency] private readonly ActionBlockerSystem _actionBlockerSystem = default!;
[Dependency] private readonly IAdminManager _adminManager = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<ActivatableUIComponent, ActivateInWorldEvent>(OnActivate);
SubscribeLocalEvent<ActivatableUIComponent, UseInHandEvent>(OnUseInHand);
SubscribeLocalEvent<ActivatableUIComponent, HandDeselectedEvent>((uid, aui, _) => CloseAll(uid, aui));
SubscribeLocalEvent<ActivatableUIComponent, UnequippedHandEvent>((uid, aui, _) => CloseAll(uid, aui));
// *THIS IS A BLATANT WORKAROUND!* RATIONALE: Microwaves need it
SubscribeLocalEvent<ActivatableUIComponent, EntParentChangedMessage>(OnParentChanged);
SubscribeLocalEvent<ActivatableUIComponent, BoundUIClosedEvent>(OnUIClose);
}
private void OnActivate(EntityUid uid, ActivatableUIComponent component, ActivateInWorldEvent args)
{
if (args.Handled) return;
if (component.InHandsOnly) return;
args.Handled = InteractUI(args.User, component);
}
private void OnUseInHand(EntityUid uid, ActivatableUIComponent component, UseInHandEvent args)
{
if (args.Handled) return;
args.Handled = InteractUI(args.User, component);
}
private void OnParentChanged(EntityUid uid, ActivatableUIComponent aui, ref EntParentChangedMessage args)
{
CloseAll(uid, aui);
}
private void OnUIClose(EntityUid uid, ActivatableUIComponent component, BoundUIClosedEvent args)
{
if (args.Session != component.CurrentSingleUser) return;
if (args.UiKey != component.Key) return;
SetCurrentSingleUser(uid, null, component);
}
private bool InteractUI(IEntity user, ActivatableUIComponent aui)
{
if (!user.TryGetComponent(out ActorComponent? actor)) return false;
if (aui.AdminOnly && !_adminManager.IsAdmin(actor.PlayerSession)) return false;
if (!_actionBlockerSystem.CanInteract(user.Uid))
{
user.PopupMessageCursor(Loc.GetString("base-computer-ui-component-cannot-interact"));
return true;
}
var ui = aui.UserInterface;
if (ui == null) return false;
if (aui.SingleUser && (aui.CurrentSingleUser != null) && (actor.PlayerSession != aui.CurrentSingleUser))
{
// If we get here, supposedly, the object is in use.
// Check with BUI that it's ACTUALLY in use just in case.
// Since this could brick the object if it goes wrong.
if (ui.SubscribedSessions.Count != 0) return false;
}
// If we've gotten this far, fire a cancellable event that indicates someone is about to activate this.
// This is so that stuff can require further conditions (like power).
var oae = new ActivatableUIOpenAttemptEvent(user);
RaiseLocalEvent(aui.OwnerUid, oae, false);
if (oae.Cancelled) return false;
SetCurrentSingleUser(aui.OwnerUid, actor.PlayerSession, aui);
ui.Toggle(actor.PlayerSession);
return true;
}
public void SetCurrentSingleUser(EntityUid uid, IPlayerSession? v, ActivatableUIComponent? aui = null)
{
if (!Resolve(uid, ref aui))
return;
if (!aui.SingleUser)
return;
aui.CurrentSingleUser = v;
RaiseLocalEvent(uid, new ActivatableUIPlayerChangedEvent(), false);
}
public override void Update(float frameTime)
{
base.Update(frameTime);
foreach (var component in EntityManager.EntityQuery<ActivatableUIComponent>(true))
{
var ui = component.UserInterface;
if (ui == null) continue;
// Done to skip an allocation on anything that's not in use.
if (ui.SubscribedSessions.Count == 0) continue;
// Must ToList in order to close things safely.
foreach (var session in ui.SubscribedSessions.ToArray())
{
if (session.AttachedEntityUid == null || !_actionBlockerSystem.CanInteract(session.AttachedEntityUid.Value))
{
ui.Close(session);
}
}
}
}
public void CloseAll(EntityUid uid, ActivatableUIComponent? aui = null)
{
if (!Resolve(uid, ref aui, false)) return;
aui.UserInterface?.CloseAll();
}
}
public class ActivatableUIOpenAttemptEvent : CancellableEntityEventArgs
{
public IEntity User { get; }
public ActivatableUIOpenAttemptEvent(IEntity who)
{
User = who;
}
}
public class ActivatableUIPlayerChangedEvent : EntityEventArgs
{
}
}

View File

@@ -5,7 +5,10 @@
description: That's an instrument.
components:
- type: Instrument
handheld: true
- type: ActivatableUI
inHandsOnly: true
singleUser: true
key: enum.InstrumentUiKey.Key
- type: UserInterface
interfaces:
- key: enum.InstrumentUiKey.Key

View File

@@ -5,7 +5,10 @@
abstract: true
components:
- type: Instrument
handheld: false
- type: ActivatableUI
inHandsOnly: false
singleUser: true
key: enum.InstrumentUiKey.Key
- type: InteractionOutline
- type: Rotatable
rotateWhileAnchored: true

View File

@@ -126,6 +126,9 @@
- type: AccessReader
access: [["HeadOfPersonnel"]]
- type: IdCardConsole
- type: ActivatableUI
key: enum.IdCardConsoleUiKey.Key
- type: ActivatableUIRequiresPower
- type: UserInterface
interfaces:
- key: enum.IdCardConsoleUiKey.Key
@@ -177,6 +180,9 @@
key: generic_key
screen: comm
- type: CommunicationsConsole
- type: ActivatableUI
key: enum.CommunicationsConsoleUiKey.Key
- type: ActivatableUIRequiresPower
- type: UserInterface
interfaces:
- key: enum.CommunicationsConsoleUiKey.Key
@@ -200,6 +206,9 @@
key: generic_key
screen: solar_screen
- type: SolarControlConsole
- type: ActivatableUI
key: enum.SolarControlConsoleUiKey.Key
- type: ActivatableUIRequiresPower
- type: UserInterface
interfaces:
- key: enum.SolarControlConsoleUiKey.Key
@@ -278,6 +287,9 @@
# - AtmosphericsWaterVapor
# - AtmosphericsPlasma
# - AtmosphericsTritium
- type: ActivatableUI
key: enum.CargoConsoleUiKey.Key
- type: ActivatableUIRequiresPower
- type: UserInterface
interfaces:
- key: enum.CargoConsoleUiKey.Key