]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Feature/door remote radial (#36378)
authorFildrance <fildrance@gmail.com>
Tue, 21 Oct 2025 12:16:38 +0000 (15:16 +0300)
committerGitHub <noreply@github.com>
Tue, 21 Oct 2025 12:16:38 +0000 (12:16 +0000)
* it works! kinda

* so it works now

* minor cleanup

* central button now is useful too

* more cleanup

* minor cleanup

* more cleanup

* refactor: migrated code from toolbox (as it was rejected as too specific)

* feat: moved border drawing for radial menu into RadialMenuTextureButton. Radial menu position setting into was moved to OverrideArrange to not being called on every frame

* refactor: major reworks!

* renamed DrawBagleSector to DrawAnnulusSector

* Remove strange indexing

* Regularize math

* refactor: re-orienting segment elements to be Y-mirrored

* refactor: extracted radial menu radius multiplier property, changed color pallet for radial menu button

* refactor: removed icon backgrounds on textures used in current radial menu buttons with sectors, RadialContainer Radius renamed and now actually changed control radius.

* refactor: in RadialMenuTextureButtonWithSector all sector colors are converted to and from sRGB in property getter-setters

* refactor: renamed srgb to include Srgb suffix so devs gonna see that its srgb clearly

* fix: enabled any functional keys pressed when pushing radial menu buttons

* fix: radial menu sector now scales with UIScale

* fix: accept only one event when clicking on radial menu ContextualButton

* fix: now radial menu buttons accepts only click/alt-click, now clicks outside menu closes menu always

* feat: simple radial menu prototype for easier creation

* refactor: cleanup, restored emote filtering, button models now have class hierarchy

* refactor: remove usage of closure from 'outside code'

* refactor: remove non existing type from UiControlTest

* refactor: remove unused using

* refactor: revert ability to declare radial menu layers in xaml, scale 32px sprites using scale in radial menu

* refactor: whitespaces

* feat: now door remote have some kind of ui to switch mode

* refactor: subscribe for dispose on existing radial menus

* feat: now simple radial menu button models can have custom color for each sector background (and hover background color). Also added OpenOverMouseScreenPosition inside SimpleRadialMenu

* fix: AI door menu now can be closed by verb if it gets unpowered

* refactor: simplify code for DoorRemoteBoundUserInterface

* fix open/close mode sprite

* remove broken merge changes

* refactor: changed DoorRemoteSystem to be fully in shared

* refactor: localize DoorRemoteBoundUserInterface

* refactor: fix multiple invocation for  TryToggleDoor inside DoorRemoteSystem on prediction

* refactor: extracted sprites and loc strings into prototype for cleaner code. Currently selected mode now have different background.

* refactor: changed hover selected color to recommmended

* refactor: reuse stylenano colors!

* review

* refactor: remove StyleNano reference

* refactor: revert removal of item status for door remote

* refactor: fix status control misprediction

* refactor: remove invalid comments, rename client DoorRemoteSystem comp after handle method

* refactor: fix DoorRemoteStatusControl not displaying status on entity pickup

---------

Co-authored-by: pa.pecherskij <pa.pecherskij@interfax.ru>
Co-authored-by: Eoin Mcloughlin <helloworld@eoinrul.es>
Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com>
Content.Client/Changeling/UI/ChangelingTransformBoundUserInterface.cs
Content.Client/Remotes/Systems/DoorRemoteSystem.cs [moved from Content.Client/Remotes/EntitySystems/DoorRemoteSystem.cs with 50% similarity]
Content.Client/Remotes/UI/DoorRemoteBoundUserInterface.cs [new file with mode: 0644]
Content.Client/Remotes/UI/DoorRemoteStatusControl.cs
Content.Server/Remotes/DoorRemoteSystem.cs
Content.Shared/Remotes/Components/DoorRemoteComponent.cs
Content.Shared/Remotes/EntitySystems/SharedDoorRemoteSystem.cs
Resources/Prototypes/Entities/Objects/Devices/door_remote.yml

index 97c07dd8c95d8c4f33186fa8098edc1a5a65bc3d..64d809c0c5fd2c0419d8a4cb3fd44d4cb11e9cf8 100644 (file)
@@ -1,4 +1,4 @@
-using Content.Client.Stylesheets;
+using Content.Client.Stylesheets.Palette;
 using Content.Client.UserInterface.Controls;
 using Content.Shared.Changeling.Components;
 using Content.Shared.Changeling.Systems;
@@ -11,8 +11,8 @@ namespace Content.Client.Changeling.UI;
 public sealed partial class ChangelingTransformBoundUserInterface(EntityUid owner, Enum uiKey) : BoundUserInterface(owner, uiKey)
 {
     private SimpleRadialMenu? _menu;
-    private static readonly Color SelectedOptionBackground = StyleNano.ButtonColorGoodDefault.WithAlpha(128);
-    private static readonly Color SelectedOptionHoverBackground = StyleNano.ButtonColorGoodHovered.WithAlpha(128);
+    private static readonly Color SelectedOptionBackground = Palettes.Green.Element.WithAlpha(128);
+    private static readonly Color SelectedOptionHoverBackground = Palettes.Green.HoveredElement.WithAlpha(128);
 
     protected override void Open()
     {
similarity index 50%
rename from Content.Client/Remotes/EntitySystems/DoorRemoteSystem.cs
rename to Content.Client/Remotes/Systems/DoorRemoteSystem.cs
index d6a9057f08e3170746d01ef3ef0bd7b2400597e5..be2e895cea5a559af4d34af14f16a18e616d1e70 100644 (file)
@@ -1,9 +1,9 @@
-using Content.Client.Remote.UI;
 using Content.Client.Items;
-using Content.Shared.Remotes.EntitySystems;
+using Content.Client.Remotes.UI;
 using Content.Shared.Remotes.Components;
+using Content.Shared.Remotes.EntitySystems;
 
-namespace Content.Client.Remotes.EntitySystems;
+namespace Content.Client.Remotes.Systems;
 
 public sealed class DoorRemoteSystem : SharedDoorRemoteSystem
 {
@@ -12,5 +12,11 @@ public sealed class DoorRemoteSystem : SharedDoorRemoteSystem
         base.Initialize();
 
         Subs.ItemStatus<DoorRemoteComponent>(ent => new DoorRemoteStatusControl(ent));
+        SubscribeLocalEvent<DoorRemoteComponent, AfterAutoHandleStateEvent>(OnAutoHandleState);
+    }
+
+    private void OnAutoHandleState(Entity<DoorRemoteComponent> ent, ref AfterAutoHandleStateEvent args)
+    {
+        ent.Comp.IsStatusControlUpdateRequired = true;
     }
 }
diff --git a/Content.Client/Remotes/UI/DoorRemoteBoundUserInterface.cs b/Content.Client/Remotes/UI/DoorRemoteBoundUserInterface.cs
new file mode 100644 (file)
index 0000000..d2ed89c
--- /dev/null
@@ -0,0 +1,63 @@
+using Content.Client.Stylesheets.Palette;
+using Content.Client.UserInterface.Controls;
+using Content.Shared.Remotes.Components;
+using Content.Shared.Remotes.EntitySystems;
+using Robust.Client.UserInterface;
+
+namespace Content.Client.Remotes.UI;
+
+public sealed class DoorRemoteBoundUserInterface(EntityUid owner, Enum uiKey) : BoundUserInterface(owner, uiKey)
+{
+    private static readonly Color SelectedOptionColor = Palettes.Green.Element.WithAlpha(128);
+    private static readonly Color SelectedOptionHoverColor = Palettes.Green.HoveredElement.WithAlpha(128);
+
+    private SimpleRadialMenu? _menu;
+
+    protected override void Open()
+    {
+        base.Open();
+
+        if (!EntMan.TryGetComponent<DoorRemoteComponent>(Owner, out var remote))
+            return;
+
+        _menu = this.CreateWindow<SimpleRadialMenu>();
+        var models = CreateButtons(remote.Mode, remote.Options);
+        _menu.SetButtons(models);
+
+        _menu.OpenOverMouseScreenPosition();
+    }
+
+    private IEnumerable<RadialMenuOptionBase> CreateButtons(OperatingMode selectedMode, List<DoorRemoteModeInfo> modeOptions)
+    {
+        var options = new List<RadialMenuOptionBase>();
+        for (var i = 0; i < modeOptions.Count; i++)
+        {
+            var modeOption = modeOptions[i];
+
+            Color? optionCustomColor = null;
+            Color? optionHoverCustomColor = null;
+            if (modeOption.Mode == selectedMode)
+            {
+                optionCustomColor = SelectedOptionColor;
+                optionHoverCustomColor = SelectedOptionHoverColor;
+            }
+
+            var option = new RadialMenuActionOption<OperatingMode>(HandleRadialMenuClick, modeOption.Mode)
+            {
+                IconSpecifier = RadialMenuIconSpecifier.With(modeOption.Icon),
+                ToolTip = Loc.GetString(modeOption.Tooltip),
+                BackgroundColor = optionCustomColor,
+                HoverBackgroundColor = optionHoverCustomColor
+            };
+            options.Add(option);
+        }
+
+        return options;
+    }
+
+    private void HandleRadialMenuClick(OperatingMode mode)
+    {
+        var msg = new DoorRemoteModeChangeMessage { Mode = mode };
+        SendPredictedMessage(msg);
+    }
+}
index 91d9667f83fc58db24ca620e9d825844d7466e86..e96cce5b2519bc2c51d77430a3086ab18a898b07 100644 (file)
@@ -1,39 +1,36 @@
 using Content.Client.Message;
 using Content.Client.Stylesheets;
 using Content.Shared.Remotes.Components;
-using Robust.Client.UserInterface;
 using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface;
 using Robust.Shared.Timing;
 
-namespace Content.Client.Remote.UI;
+namespace Content.Client.Remotes.UI;
 
-public sealed class DoorRemoteStatusControl : Control
+public sealed class DoorRemoteStatusControl(Entity<DoorRemoteComponent> ent) : Control
 {
-    private readonly Entity<DoorRemoteComponent> _entity;
-    private readonly RichTextLabel _label;
-
-    // set to toggle bolts initially just so that it updates on first pickup of remote
-    private OperatingMode PrevOperatingMode = OperatingMode.placeholderForUiUpdates;
-
-    public DoorRemoteStatusControl(Entity<DoorRemoteComponent> entity)
-    {
-        _entity = entity;
-        _label = new RichTextLabel { StyleClasses = { StyleClass.ItemStatus } };
-        AddChild(_label);
-    }
+    private RichTextLabel? _label;
 
     protected override void FrameUpdate(FrameEventArgs args)
     {
         base.FrameUpdate(args);
 
-        // only updates the UI if any of the details are different than they previously were
-        if (PrevOperatingMode == _entity.Comp.Mode)
+        if (_label == null)
+        {
+            _label = new RichTextLabel { StyleClasses = { StyleClass.ItemStatus } };
+            AddChild(_label);
+        }
+        else if (!ent.Comp.IsStatusControlUpdateRequired)
             return;
 
-        PrevOperatingMode = _entity.Comp.Mode;
+        UpdateLabel(_label);
 
-        // Update current volume and injector state
-        var modeStringLocalized = Loc.GetString(_entity.Comp.Mode switch
+        ent.Comp.IsStatusControlUpdateRequired = false;
+    }
+
+    private void UpdateLabel(RichTextLabel label)
+    {
+        var modeStringLocalized = Loc.GetString(ent.Comp.Mode switch
         {
             OperatingMode.OpenClose => "door-remote-open-close-text",
             OperatingMode.ToggleBolts => "door-remote-toggle-bolt-text",
@@ -41,6 +38,6 @@ public sealed class DoorRemoteStatusControl : Control
             _ => "door-remote-invalid-text"
         });
 
-        _label.SetMarkup(Loc.GetString("door-remote-mode-label", ("modeString", modeStringLocalized)));
+        label.SetMarkup(Loc.GetString("door-remote-mode-label", ("modeString", modeStringLocalized)));
     }
 }
index c3425f347ae46a11ec1cb3db8c4d052d7e584f82..6d2219bae13449421321e2c46ed5ead4ddf78d66 100644 (file)
@@ -1,108 +1,5 @@
-using Content.Server.Administration.Logs;
-using Content.Server.Doors.Systems;
-using Content.Server.Power.EntitySystems;
-using Content.Shared.Access.Components;
-using Content.Shared.Database;
-using Content.Shared.Doors.Components;
-using Content.Shared.Examine;
-using Content.Shared.Interaction;
-using Content.Shared.Remotes.Components;
 using Content.Shared.Remotes.EntitySystems;
 
-namespace Content.Shared.Remotes
-{
-    public sealed class DoorRemoteSystem : SharedDoorRemoteSystem
-    {
-        [Dependency] private readonly IAdminLogManager _adminLogger = default!;
-        [Dependency] private readonly AirlockSystem _airlock = default!;
-        [Dependency] private readonly DoorSystem _doorSystem = default!;
-        [Dependency] private readonly ExamineSystemShared _examine = default!;
+namespace Content.Server.Remotes;
 
-        public override void Initialize()
-        {
-            base.Initialize();
-
-            SubscribeLocalEvent<DoorRemoteComponent, BeforeRangedInteractEvent>(OnBeforeInteract);
-        }
-
-        private void OnBeforeInteract(Entity<DoorRemoteComponent> entity, ref BeforeRangedInteractEvent args)
-        {
-            bool isAirlock = TryComp<AirlockComponent>(args.Target, out var airlockComp);
-
-            if (args.Handled
-                || args.Target == null
-                || !TryComp<DoorComponent>(args.Target, out var doorComp) // If it isn't a door we don't use it
-                // Only able to control doors if they are within your vision and within your max range.
-                // Not affected by mobs or machines anymore.
-                || !_examine.InRangeUnOccluded(args.User,
-                    args.Target.Value,
-                    SharedInteractionSystem.MaxRaycastRange,
-                    null))
-
-            {
-                return;
-            }
-
-            args.Handled = true;
-
-            if (!this.IsPowered(args.Target.Value, EntityManager))
-            {
-                Popup.PopupEntity(Loc.GetString("door-remote-no-power"), args.User, args.User);
-                return;
-            }
-
-            var accessTarget = args.Used;
-            // This covers the accesses the REMOTE has, and is not effected by the user's ID card.
-            if (entity.Comp.IncludeUserAccess) // Allows some door remotes to inherit the user's access.
-            {
-                accessTarget = args.User;
-                // This covers the accesses the USER has, which always includes the remote's access since holding a remote acts like holding an ID card.
-            }
-
-            if (TryComp<AccessReaderComponent>(args.Target, out var accessComponent)
-                && !_doorSystem.HasAccess(args.Target.Value, accessTarget, doorComp, accessComponent))
-            {
-                if (isAirlock)
-                    _doorSystem.Deny(args.Target.Value, doorComp, accessTarget);
-                Popup.PopupEntity(Loc.GetString("door-remote-denied"), args.User, args.User);
-                return;
-            }
-
-            switch (entity.Comp.Mode)
-            {
-                case OperatingMode.OpenClose:
-                    if (_doorSystem.TryToggleDoor(args.Target.Value, doorComp, accessTarget))
-                        _adminLogger.Add(LogType.Action,
-                            LogImpact.Medium,
-                            $"{ToPrettyString(args.User):player} used {ToPrettyString(args.Used)} on {ToPrettyString(args.Target.Value)}: {doorComp.State}");
-                    break;
-                case OperatingMode.ToggleBolts:
-                    if (TryComp<DoorBoltComponent>(args.Target, out var boltsComp))
-                    {
-                        if (!boltsComp.BoltWireCut)
-                        {
-                            _doorSystem.SetBoltsDown((args.Target.Value, boltsComp), !boltsComp.BoltsDown, accessTarget);
-                            _adminLogger.Add(LogType.Action,
-                                LogImpact.Medium,
-                                $"{ToPrettyString(args.User):player} used {ToPrettyString(args.Used)} on {ToPrettyString(args.Target.Value)} to {(boltsComp.BoltsDown ? "" : "un")}bolt it");
-                        }
-                    }
-
-                    break;
-                case OperatingMode.ToggleEmergencyAccess:
-                    if (airlockComp != null)
-                    {
-                        _airlock.SetEmergencyAccess((args.Target.Value, airlockComp), !airlockComp.EmergencyAccess);
-                        _adminLogger.Add(LogType.Action,
-                            LogImpact.Medium,
-                            $"{ToPrettyString(args.User):player} used {ToPrettyString(args.Used)} on {ToPrettyString(args.Target.Value)} to set emergency access {(airlockComp.EmergencyAccess ? "on" : "off")}");
-                    }
-
-                    break;
-                default:
-                    throw new InvalidOperationException(
-                        $"{nameof(DoorRemoteComponent)} had invalid mode {entity.Comp.Mode}");
-            }
-        }
-    }
-}
+public sealed class DoorRemoteSystem : SharedDoorRemoteSystem;
index 64977596c2829abc94fee3825291c002208362cb..7bce64262d1c77935b04dc22329843247aa405c3 100644 (file)
@@ -1,26 +1,74 @@
 using Robust.Shared.GameStates;
+using Robust.Shared.Utility;
+using Robust.Shared.Serialization;
 
 namespace Content.Shared.Remotes.Components;
 
-[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
+/// <summary>
+/// Component for door remote devices, that allow you to control doors from a distance.
+/// </summary>
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(true)]
 public sealed partial class DoorRemoteComponent : Component
 {
-    [AutoNetworkedField]
-    [DataField]
+    /// <summary>
+    /// Currently selected mode. The mode dictates what device would do upon
+    /// interaction with door.
+    /// </summary>
+    [DataField, AutoNetworkedField]
     public OperatingMode Mode = OperatingMode.OpenClose;
 
     /// <summary>
-    /// Does the remote allow the user to manipulate doors that they have access to, even if the remote itself does not?
+    /// Modes with metadata that could be displayed in the device mode change menu.
     /// </summary>
-    [AutoNetworkedField]
     [DataField]
-    public bool IncludeUserAccess = false;
+    public List<DoorRemoteModeInfo> Options;
+
+    /// <summary>
+    /// Does the remote allow the user to control doors that they have access to, even if the remote itself does not?
+    /// </summary>
+    [DataField, AutoNetworkedField]
+    public bool IncludeUserAccess;
+
+    /// <summary>
+    /// Client-side only field for checking if StatusControl requires update.
+    /// </summary>
+    /// <remarks>
+    /// StatusControl is updated inside loop and cannot understand
+    /// when state is of component it looks for is restored, thus mispredicting. To avoid that,
+    /// client-side system basically controls behaviour of StatusControl updates using this field.
+    /// </remarks>
+    public bool IsStatusControlUpdateRequired;
+}
+
+/// <summary>
+/// Remote door device mode with data that is required for menu display.
+/// </summary>
+[DataDefinition]
+public sealed partial class DoorRemoteModeInfo
+{
+    /// <summary>
+    /// Icon that should represent the option in the radial menu.
+    /// </summary>
+    [DataField(required: true)]
+    public SpriteSpecifier Icon = default!;
+
+    /// <summary>
+    /// Tooltip describing the option in the radial menu.
+    /// </summary>
+    [DataField(required: true)]
+    public LocId Tooltip;
+
+    /// <summary>
+    /// Mode option.
+    /// </summary>
+    [DataField(required: true)]
+    public OperatingMode Mode;
 }
 
+[Serializable, NetSerializable]
 public enum OperatingMode : byte
 {
     OpenClose,
     ToggleBolts,
-    ToggleEmergencyAccess,
-    placeholderForUiUpdates
+    ToggleEmergencyAccess
 }
index e9bbd27ada407829e598b5ee2683b97dd5446754..67c2214ca2f1dec4c155637d702fb77b9c59fe9e 100644 (file)
+using Content.Shared.Access.Components;
+using Content.Shared.Administration.Logs;
+using Content.Shared.Database;
+using Content.Shared.Doors.Components;
+using Content.Shared.Doors.Systems;
+using Content.Shared.Examine;
+using Content.Shared.Interaction;
 using Content.Shared.Popups;
-using Content.Shared.Interaction.Events;
+using Content.Shared.Power.EntitySystems;
 using Content.Shared.Remotes.Components;
+using Robust.Shared.Serialization;
+using Robust.Shared.Timing;
 
 namespace Content.Shared.Remotes.EntitySystems;
 
 public abstract class SharedDoorRemoteSystem : EntitySystem
 {
-    [Dependency] protected readonly SharedPopupSystem Popup = default!;
+    [Dependency] private readonly SharedAirlockSystem _airlock = default!;
+    [Dependency] private readonly SharedDoorSystem _doorSystem = default!;
+    [Dependency] private readonly ExamineSystemShared _examine = default!;
+    [Dependency] private readonly SharedPowerReceiverSystem _powerReceiver = default!;
+    [Dependency] private readonly SharedPopupSystem _popup = default!;
+    [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
+    [Dependency] protected readonly IGameTiming Timing = default!;
+
 
     public override void Initialize()
     {
-        SubscribeLocalEvent<DoorRemoteComponent, UseInHandEvent>(OnInHandActivation);
+        SubscribeLocalEvent<DoorRemoteComponent, DoorRemoteModeChangeMessage>(OnDoorRemoteModeChange);
+        SubscribeLocalEvent<DoorRemoteComponent, BeforeRangedInteractEvent>(OnBeforeInteract);
+    }
+
+    private void OnDoorRemoteModeChange(Entity<DoorRemoteComponent> ent, ref DoorRemoteModeChangeMessage args)
+    {
+        ent.Comp.Mode = args.Mode;
+        Dirty(ent);
     }
 
-    private void OnInHandActivation(Entity<DoorRemoteComponent> entity, ref UseInHandEvent args)
+    private void OnBeforeInteract(Entity<DoorRemoteComponent> entity, ref BeforeRangedInteractEvent args)
     {
-        string switchMessageId;
+        if (!Timing.IsFirstTimePredicted)
+            return;
+
+        var isAirlock = TryComp<AirlockComponent>(args.Target, out var airlockComp);
+
+        if (args.Handled
+            || args.Target == null
+            || !TryComp<DoorComponent>(args.Target, out var doorComp) // If it isn't a door we don't use it
+                                                                      // Only able to control doors if they are within your vision and within your max range.
+                                                                      // Not affected by mobs or machines anymore.
+            || !_examine.InRangeUnOccluded(args.User,
+                args.Target.Value,
+                SharedInteractionSystem.MaxRaycastRange,
+                null))
+
+        {
+            return;
+        }
+
+        args.Handled = true;
+
+        if (!_powerReceiver.IsPowered(args.Target.Value))
+        {
+            _popup.PopupClient(Loc.GetString("door-remote-no-power"), args.User, args.User);
+            return;
+        }
+
+        var accessTarget = args.Used;
+        // This covers the accesses the REMOTE has, and is not effected by the user's ID card.
+        if (entity.Comp.IncludeUserAccess) // Allows some door remotes to inherit the user's access.
+        {
+            accessTarget = args.User;
+            // This covers the accesses the USER has, which always includes the remote's access since holding a remote acts like holding an ID card.
+        }
+
+        if (TryComp<AccessReaderComponent>(args.Target, out var accessComponent)
+            && !_doorSystem.HasAccess(args.Target.Value, accessTarget, doorComp, accessComponent))
+        {
+            if (isAirlock)
+                _doorSystem.Deny(args.Target.Value, doorComp, user: args.User, predicted: true);
+
+            _popup.PopupClient(Loc.GetString("door-remote-denied"), args.User, args.User);
+            return;
+        }
+
         switch (entity.Comp.Mode)
         {
             case OperatingMode.OpenClose:
-                entity.Comp.Mode = OperatingMode.ToggleBolts;
-                switchMessageId = "door-remote-switch-state-toggle-bolts";
+                if (_doorSystem.TryToggleDoor(args.Target.Value, doorComp, user: args.User, predicted: true))
+                    _adminLogger.Add(LogType.Action,
+                        LogImpact.Medium,
+                        $"{ToPrettyString(args.User):player} used {ToPrettyString(args.Used)} on {ToPrettyString(args.Target.Value)}: {doorComp.State}");
                 break;
-
-            // Skip toggle bolts mode and move on from there (to emergency access)
             case OperatingMode.ToggleBolts:
-                entity.Comp.Mode = OperatingMode.ToggleEmergencyAccess;
-                switchMessageId = "door-remote-switch-state-toggle-emergency-access";
-                break;
+                if (TryComp<DoorBoltComponent>(args.Target, out var boltsComp))
+                {
+                    if (!boltsComp.BoltWireCut)
+                    {
+                        _doorSystem.SetBoltsDown((args.Target.Value, boltsComp), !boltsComp.BoltsDown, user: args.User, predicted: true);
+                        _adminLogger.Add(LogType.Action,
+                            LogImpact.Medium,
+                            $"{ToPrettyString(args.User):player} used {ToPrettyString(args.Used)} on {ToPrettyString(args.Target.Value)} to {(boltsComp.BoltsDown ? "" : "un")}bolt it");
+                    }
+                }
 
-            // Skip ToggleEmergencyAccess mode and move on from there (to door toggle)
+                break;
             case OperatingMode.ToggleEmergencyAccess:
-                entity.Comp.Mode = OperatingMode.OpenClose;
-                switchMessageId = "door-remote-switch-state-open-close";
+                if (airlockComp != null)
+                {
+                    _airlock.SetEmergencyAccess((args.Target.Value, airlockComp), !airlockComp.EmergencyAccess, user: args.User, predicted: true);
+                    _adminLogger.Add(LogType.Action,
+                        LogImpact.Medium,
+                        $"{ToPrettyString(args.User):player} used {ToPrettyString(args.Used)} on {ToPrettyString(args.Target.Value)} to set emergency access {(airlockComp.EmergencyAccess ? "on" : "off")}");
+                }
+
                 break;
             default:
                 throw new InvalidOperationException(
                     $"{nameof(DoorRemoteComponent)} had invalid mode {entity.Comp.Mode}");
         }
-        Dirty(entity);
-        Popup.PopupClient(Loc.GetString(switchMessageId), entity, args.User);
     }
 }
+
+[Serializable, NetSerializable]
+public sealed class DoorRemoteModeChangeMessage : BoundUserInterfaceMessage
+{
+    public OperatingMode Mode;
+}
+
+[Serializable, NetSerializable]
+public enum DoorRemoteUiKey : byte
+{
+    Key
+}
index 8f7f6957b201a1c47b670d55b04ea53b5ad5d415..963c7e68a06675baa5a57d0fe509c0fb12f257a7 100644 (file)
     storedRotation: -90
   - type: Access
   - type: DoorRemote
+    options:
+    - mode: OpenClose
+      tooltip: door-remote-open-close-text
+      icon:
+        sprite: /Textures/Structures/Doors/Airlocks/Standard/basic.rsi
+        state: assembly
+    - mode: ToggleBolts
+      tooltip: door-remote-toggle-bolt-text
+      icon:
+        sprite: /Textures/Interface/Actions/actions_ai.rsi
+        state: bolt_door
+    - mode: ToggleEmergencyAccess
+      tooltip: door-remote-emergency-access-text
+      icon:
+        sprite: /Textures/Interface/Actions/actions_ai.rsi
+        state: emergency_on
   - type: StealTarget
     stealGroup: DoorRemote
+  - type: ActivatableUI
+    inHandsOnly: true
+    key: enum.DoorRemoteUiKey.Key
+  - type: UserInterface
+    interfaces:
+      enum.DoorRemoteUiKey.Key:
+        type: DoorRemoteBoundUserInterface
 
 - type: entity
   parent: [DoorRemoteDefault, BaseCommandContraband]