-using Content.Client.Stylesheets;
+using Content.Client.Stylesheets.Palette;
using Content.Client.UserInterface.Controls;
using Content.Shared.Changeling.Components;
using Content.Shared.Changeling.Systems;
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()
{
-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
{
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;
}
}
--- /dev/null
+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);
+ }
+}
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",
_ => "door-remote-invalid-text"
});
- _label.SetMarkup(Loc.GetString("door-remote-mode-label", ("modeString", modeStringLocalized)));
+ label.SetMarkup(Loc.GetString("door-remote-mode-label", ("modeString", modeStringLocalized)));
}
}
-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;
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
}
+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
+}
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]