namespace Content.Shared.UserInterface
{
- [RegisterComponent, NetworkedComponent]
+ [RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
public sealed partial class ActivatableUIComponent : Component
{
[DataField(required: true, customTypeSerializer: typeof(EnumSerializer))]
- public Enum? Key { get; set; } = default!;
+ public Enum? Key;
+ /// <summary>
+ /// Whether the item must be held in one of the user's hands to work.
+ /// This is ignored unless <see cref="RequireHands"/> is true.
+ /// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField]
- public bool InHandsOnly { get; set; } = false;
+ public bool InHandsOnly;
[DataField]
- public bool SingleUser { get; set; } = false;
+ public bool SingleUser;
[ViewVariables(VVAccess.ReadWrite)]
[DataField]
- public bool AdminOnly { get; set; } = false;
+ public bool AdminOnly;
[DataField]
public LocId VerbText = "ui-verb-toggle-open";
/// <summary>
/// Entities that are required to open this UI.
/// </summary>
- [DataField("allowedItems")]
- [ViewVariables(VVAccess.ReadWrite)]
- public EntityWhitelist? AllowedItems = null;
+ [DataField, ViewVariables(VVAccess.ReadWrite)]
+ public EntityWhitelist? RequiredItems;
/// <summary>
- /// Whether you can activate this ui with activateinhand or not
+ /// If true, then this UI can only be opened via verbs. I.e., normal interactions/activations will not open
+ /// the UI.
/// </summary>
- [ViewVariables(VVAccess.ReadWrite)]
- [DataField]
- public bool RightClickOnly;
+ [DataField, ViewVariables(VVAccess.ReadWrite)]
+ public bool VerbOnly;
/// <summary>
/// Whether spectators (non-admin ghosts) should be allowed to view this UI.
public bool AllowSpectator = true;
/// <summary>
- /// Whether the UI should close when the item is deselected due to a hand swap or drop
+ /// Whether the item must be in the user's currently selected/active hand.
+ /// This is ignored unless <see cref="InHandsOnly"/> is true.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField]
- public bool CloseOnHandDeselect = true;
+ public bool RequireActiveHand = true;
/// <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]
+ [DataField, AutoNetworkedField]
public EntityUid? CurrentSingleUser;
}
}
using Content.Shared.Ghost;
using Content.Shared.Hands;
using Content.Shared.Hands.Components;
+using Content.Shared.Hands.EntitySystems;
using Content.Shared.Interaction;
-using Content.Shared.Interaction.Events;
using Content.Shared.Popups;
using Content.Shared.Verbs;
-using Robust.Shared.Player;
+using Robust.Shared.Containers;
namespace Content.Shared.UserInterface;
[Dependency] private readonly ActionBlockerSystem _blockerSystem = default!;
[Dependency] private readonly SharedUserInterfaceSystem _uiSystem = default!;
[Dependency] private readonly SharedPopupSystem _popupSystem = default!;
+ [Dependency] private readonly SharedHandsSystem _hands = default!;
+ [Dependency] private readonly SharedContainerSystem _container = default!;
+ [Dependency] private readonly SharedInteractionSystem _interaction = default!;
+
+ private readonly List<EntityUid> _toClose = new();
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<ActivatableUIComponent, ActivateInWorldEvent>(OnActivate);
- SubscribeLocalEvent<ActivatableUIComponent, UseInHandEvent>(OnUseInHand);
SubscribeLocalEvent<ActivatableUIComponent, InteractUsingEvent>(OnInteractUsing);
SubscribeLocalEvent<ActivatableUIComponent, HandDeselectedEvent>(OnHandDeselected);
- SubscribeLocalEvent<ActivatableUIComponent, GotUnequippedHandEvent>((uid, aui, _) => CloseAll(uid, aui));
- // *THIS IS A BLATANT WORKAROUND!* RATIONALE: Microwaves need it
- SubscribeLocalEvent<ActivatableUIComponent, EntParentChangedMessage>(OnParentChanged);
+ SubscribeLocalEvent<ActivatableUIComponent, GotUnequippedHandEvent>(OnHandUnequipped);
SubscribeLocalEvent<ActivatableUIComponent, BoundUIClosedEvent>(OnUIClose);
- SubscribeLocalEvent<BoundUserInterfaceMessageAttempt>(OnBoundInterfaceInteractAttempt);
+ SubscribeLocalEvent<ActivatableUIComponent, GetVerbsEvent<ActivationVerb>>(GetActivationVerb);
+ SubscribeLocalEvent<ActivatableUIComponent, GetVerbsEvent<Verb>>(GetVerb);
- SubscribeLocalEvent<ActivatableUIComponent, GetVerbsEvent<ActivationVerb>>(AddOpenUiVerb);
+ // TODO ActivatableUI
+ // Add UI-user component, and listen for user container changes.
+ // I.e., should lose a computer UI if a player gets shut into a locker.
+ SubscribeLocalEvent<ActivatableUIComponent, EntGotInsertedIntoContainerMessage>(OnGotInserted);
+ SubscribeLocalEvent<ActivatableUIComponent, EntGotRemovedFromContainerMessage>(OnGotRemoved);
+ SubscribeLocalEvent<BoundUserInterfaceMessageAttempt>(OnBoundInterfaceInteractAttempt);
SubscribeLocalEvent<UserInterfaceComponent, OpenUiActionEvent>(OnActionPerform);
InitializePower();
args.Handled = _uiSystem.TryToggleUi(uid, args.Key, args.Performer);
}
- private void AddOpenUiVerb(EntityUid uid, ActivatableUIComponent component, GetVerbsEvent<ActivationVerb> args)
+
+ private void GetActivationVerb(EntityUid uid, ActivatableUIComponent component, GetVerbsEvent<ActivationVerb> args)
{
- if (!args.CanAccess)
+ if (component.VerbOnly || !ShouldAddVerb(uid, component, args))
return;
- if (component.RequireHands && args.Hands == null)
- return;
+ args.Verbs.Add(new ActivationVerb
+ {
+ // TODO VERBS add "open UI" icon
+ Act = () => InteractUI(args.User, uid, component),
+ Text = Loc.GetString(component.VerbText)
+ });
+ }
- if (component.InHandsOnly && args.Using != uid)
+ private void GetVerb(EntityUid uid, ActivatableUIComponent component, GetVerbsEvent<Verb> args)
+ {
+ if (!component.VerbOnly || !ShouldAddVerb(uid, component, args))
return;
- if (!args.CanInteract && (!component.AllowSpectator || !HasComp<GhostComponent>(args.User)))
- return;
+ args.Verbs.Add(new Verb
+ {
+ // TODO VERBS add "open UI" icon
+ Act = () => InteractUI(args.User, uid, component),
+ Text = Loc.GetString(component.VerbText)
+ });
+ }
+
+ private bool ShouldAddVerb<T>(EntityUid uid, ActivatableUIComponent component, GetVerbsEvent<T> args) where T : Verb
+ {
+ if (!args.CanAccess)
+ return false;
+
+ if (component.RequireHands)
+ {
+ if (args.Hands == null)
+ return false;
+
+ if (component.InHandsOnly)
+ {
+ if (!_hands.IsHolding(args.User, uid, out var hand, args.Hands))
+ return false;
+
+ if (component.RequireActiveHand && args.Hands.ActiveHand != hand)
+ return false;
+ }
+ }
- ActivationVerb verb = new();
- verb.Act = () => InteractUI(args.User, uid, component);
- verb.Text = Loc.GetString(component.VerbText);
- // TODO VERBS add "open UI" icon?
- args.Verbs.Add(verb);
+ return args.CanInteract || component.AllowSpectator && HasComp<GhostComponent>(args.User);
}
private void OnActivate(EntityUid uid, ActivatableUIComponent component, ActivateInWorldEvent args)
if (args.Handled)
return;
- if (component.InHandsOnly)
+ if (component.VerbOnly)
return;
- if (component.AllowedItems != null)
+ if (component.RequiredItems != null)
return;
args.Handled = InteractUI(args.User, uid, component);
}
- private void OnUseInHand(EntityUid uid, ActivatableUIComponent component, UseInHandEvent args)
+ private void OnInteractUsing(EntityUid uid, ActivatableUIComponent component, InteractUsingEvent args)
{
if (args.Handled)
return;
- if (component.RightClickOnly)
+ if (component.VerbOnly)
return;
- if (component.AllowedItems != null)
+ if (component.RequiredItems == null)
return;
- args.Handled = InteractUI(args.User, uid, component);
- }
+ if (!component.RequiredItems.IsValid(args.Used, EntityManager))
+ return;
- private void OnInteractUsing(EntityUid uid, ActivatableUIComponent component, InteractUsingEvent args)
- {
- if (args.Handled) return;
- if (component.AllowedItems == null) return;
- if (!component.AllowedItems.IsValid(args.Used, EntityManager)) return;
args.Handled = InteractUI(args.User, uid, component);
}
- private void OnParentChanged(EntityUid uid, ActivatableUIComponent aui, ref EntParentChangedMessage args)
- {
- CloseAll(uid, aui);
- }
-
private void OnUIClose(EntityUid uid, ActivatableUIComponent component, BoundUIClosedEvent args)
{
var user = args.Actor;
if (!_blockerSystem.CanInteract(user, uiEntity) && (!aui.AllowSpectator || !HasComp<GhostComponent>(user)))
return false;
- if (aui.RequireHands && !HasComp<HandsComponent>(user))
- return false;
+ if (aui.RequireHands)
+ {
+ if (!TryComp(user, out HandsComponent? hands))
+ return false;
+
+ if (aui.InHandsOnly)
+ {
+ if (!_hands.IsHolding(user, uiEntity, out var hand, hands))
+ return false;
+
+ if (aui.RequireActiveHand && hands.ActiveHand != hand)
+ return false;
+ }
+ }
if (aui.AdminOnly && !_adminManager.IsAdmin(user))
return false;
if (aui.SingleUser && aui.CurrentSingleUser != null && user != aui.CurrentSingleUser)
{
- string message = Loc.GetString("machine-already-in-use", ("machine", uiEntity));
+ var message = Loc.GetString("machine-already-in-use", ("machine", uiEntity));
_popupSystem.PopupEntity(message, uiEntity, user);
- // 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 (_uiSystem.IsUiOpen(uiEntity, aui.Key))
- return false;
+ return true;
+
+ Log.Error($"Activatable UI has user without being opened? Entity: {ToPrettyString(uiEntity)}. User: {aui.CurrentSingleUser}, Key: {aui.Key}");
}
// If we've gotten this far, fire a cancellable event that indicates someone is about to activate this.
return;
aui.CurrentSingleUser = user;
+ Dirty(uid, aui);
RaiseLocalEvent(uid, new ActivatableUIPlayerChangedEvent());
}
public void CloseAll(EntityUid uid, ActivatableUIComponent? aui = null)
{
- if (!Resolve(uid, ref aui, false) || aui.Key == null)
+ if (!Resolve(uid, ref aui, false))
return;
+ if (aui.Key == null)
+ {
+ Log.Error($"Encountered null key in activatable ui on entity {ToPrettyString(uid)}");
+ return;
+ }
+
_uiSystem.CloseUi(uid, aui.Key);
}
- private void OnHandDeselected(EntityUid uid, ActivatableUIComponent? aui, HandDeselectedEvent args)
+ private void OnHandDeselected(Entity<ActivatableUIComponent> ent, ref HandDeselectedEvent args)
{
- if (!Resolve(uid, ref aui, false))
+ if (ent.Comp.RequireHands && ent.Comp.InHandsOnly && ent.Comp.RequireActiveHand)
+ CloseAll(ent, ent);
+ }
+
+ private void OnHandUnequipped(Entity<ActivatableUIComponent> ent, ref GotUnequippedHandEvent args)
+ {
+ if (ent.Comp.RequireHands && ent.Comp.InHandsOnly)
+ CloseAll(ent, ent);
+ }
+
+ private void OnGotInserted(Entity<ActivatableUIComponent> ent, ref EntGotInsertedIntoContainerMessage args)
+ {
+ CheckAccess((ent, ent));
+ }
+
+ private void OnGotRemoved(Entity<ActivatableUIComponent> ent, ref EntGotRemovedFromContainerMessage args)
+ {
+ CheckAccess((ent, ent));
+ }
+
+ public void CheckAccess(Entity<ActivatableUIComponent?> ent)
+ {
+ if (!Resolve(ent, ref ent.Comp))
return;
- if (!aui.CloseOnHandDeselect)
+ if (ent.Comp.Key == null)
+ {
+ Log.Error($"Encountered null key in activatable ui on entity {ToPrettyString(ent)}");
return;
+ }
+
+ foreach (var user in _uiSystem.GetActors(ent.Owner, ent.Comp.Key))
+ {
+ if (!_container.IsInSameOrParentContainer(user, ent)
+ && !_interaction.CanAccessViaStorage(user, ent))
+ {
+ _toClose.Add(user);
+ continue;
+
+ }
+
+ if (!_interaction.InRangeUnobstructed(user, ent))
+ _toClose.Add(user);
+ }
+
+ foreach (var user in _toClose)
+ {
+ _uiSystem.CloseUi(ent.Owner, ent.Comp.Key, user);
+ }
- CloseAll(uid, aui);
+ _toClose.Clear();
}
}