diff --git a/Content.Client/Changeling/UI/ChangelingTransformBoundUserInterface.cs b/Content.Client/Changeling/UI/ChangelingTransformBoundUserInterface.cs index 97c07dd8c9..64d809c0c5 100644 --- a/Content.Client/Changeling/UI/ChangelingTransformBoundUserInterface.cs +++ b/Content.Client/Changeling/UI/ChangelingTransformBoundUserInterface.cs @@ -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() { diff --git a/Content.Client/Remotes/EntitySystems/DoorRemoteSystem.cs b/Content.Client/Remotes/Systems/DoorRemoteSystem.cs similarity index 50% rename from Content.Client/Remotes/EntitySystems/DoorRemoteSystem.cs rename to Content.Client/Remotes/Systems/DoorRemoteSystem.cs index d6a9057f08..be2e895cea 100644 --- a/Content.Client/Remotes/EntitySystems/DoorRemoteSystem.cs +++ b/Content.Client/Remotes/Systems/DoorRemoteSystem.cs @@ -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(ent => new DoorRemoteStatusControl(ent)); + SubscribeLocalEvent(OnAutoHandleState); + } + + private void OnAutoHandleState(Entity 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 index 0000000000..d2ed89c100 --- /dev/null +++ b/Content.Client/Remotes/UI/DoorRemoteBoundUserInterface.cs @@ -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(Owner, out var remote)) + return; + + _menu = this.CreateWindow(); + var models = CreateButtons(remote.Mode, remote.Options); + _menu.SetButtons(models); + + _menu.OpenOverMouseScreenPosition(); + } + + private IEnumerable CreateButtons(OperatingMode selectedMode, List modeOptions) + { + var options = new List(); + 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(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); + } +} diff --git a/Content.Client/Remotes/UI/DoorRemoteStatusControl.cs b/Content.Client/Remotes/UI/DoorRemoteStatusControl.cs index 91d9667f83..e96cce5b25 100644 --- a/Content.Client/Remotes/UI/DoorRemoteStatusControl.cs +++ b/Content.Client/Remotes/UI/DoorRemoteStatusControl.cs @@ -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 ent) : Control { - private readonly Entity _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 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))); } } diff --git a/Content.Server/Remotes/DoorRemoteSystem.cs b/Content.Server/Remotes/DoorRemoteSystem.cs index c3425f347a..6d2219bae1 100644 --- a/Content.Server/Remotes/DoorRemoteSystem.cs +++ b/Content.Server/Remotes/DoorRemoteSystem.cs @@ -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(OnBeforeInteract); - } - - private void OnBeforeInteract(Entity entity, ref BeforeRangedInteractEvent args) - { - bool isAirlock = TryComp(args.Target, out var airlockComp); - - if (args.Handled - || args.Target == null - || !TryComp(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(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(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; diff --git a/Content.Shared/Remotes/Components/DoorRemoteComponent.cs b/Content.Shared/Remotes/Components/DoorRemoteComponent.cs index 64977596c2..7bce64262d 100644 --- a/Content.Shared/Remotes/Components/DoorRemoteComponent.cs +++ b/Content.Shared/Remotes/Components/DoorRemoteComponent.cs @@ -1,26 +1,74 @@ using Robust.Shared.GameStates; +using Robust.Shared.Utility; +using Robust.Shared.Serialization; namespace Content.Shared.Remotes.Components; -[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +/// +/// Component for door remote devices, that allow you to control doors from a distance. +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(true)] public sealed partial class DoorRemoteComponent : Component { - [AutoNetworkedField] - [DataField] + /// + /// Currently selected mode. The mode dictates what device would do upon + /// interaction with door. + /// + [DataField, AutoNetworkedField] public OperatingMode Mode = OperatingMode.OpenClose; /// - /// 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. /// - [AutoNetworkedField] [DataField] - public bool IncludeUserAccess = false; + public List Options; + + /// + /// Does the remote allow the user to control doors that they have access to, even if the remote itself does not? + /// + [DataField, AutoNetworkedField] + public bool IncludeUserAccess; + + /// + /// Client-side only field for checking if StatusControl requires update. + /// + /// + /// 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. + /// + public bool IsStatusControlUpdateRequired; } +/// +/// Remote door device mode with data that is required for menu display. +/// +[DataDefinition] +public sealed partial class DoorRemoteModeInfo +{ + /// + /// Icon that should represent the option in the radial menu. + /// + [DataField(required: true)] + public SpriteSpecifier Icon = default!; + + /// + /// Tooltip describing the option in the radial menu. + /// + [DataField(required: true)] + public LocId Tooltip; + + /// + /// Mode option. + /// + [DataField(required: true)] + public OperatingMode Mode; +} + +[Serializable, NetSerializable] public enum OperatingMode : byte { OpenClose, ToggleBolts, - ToggleEmergencyAccess, - placeholderForUiUpdates + ToggleEmergencyAccess } diff --git a/Content.Shared/Remotes/EntitySystems/SharedDoorRemoteSystem.cs b/Content.Shared/Remotes/EntitySystems/SharedDoorRemoteSystem.cs index e9bbd27ada..67c2214ca2 100644 --- a/Content.Shared/Remotes/EntitySystems/SharedDoorRemoteSystem.cs +++ b/Content.Shared/Remotes/EntitySystems/SharedDoorRemoteSystem.cs @@ -1,44 +1,134 @@ +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(OnInHandActivation); + SubscribeLocalEvent(OnDoorRemoteModeChange); + SubscribeLocalEvent(OnBeforeInteract); } - private void OnInHandActivation(Entity entity, ref UseInHandEvent args) + private void OnDoorRemoteModeChange(Entity ent, ref DoorRemoteModeChangeMessage args) { - string switchMessageId; + ent.Comp.Mode = args.Mode; + Dirty(ent); + } + + private void OnBeforeInteract(Entity entity, ref BeforeRangedInteractEvent args) + { + if (!Timing.IsFirstTimePredicted) + return; + + var isAirlock = TryComp(args.Target, out var airlockComp); + + if (args.Handled + || args.Target == null + || !TryComp(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(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(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 +} diff --git a/Resources/Prototypes/Entities/Objects/Devices/door_remote.yml b/Resources/Prototypes/Entities/Objects/Devices/door_remote.yml index 8f7f6957b2..963c7e68a0 100644 --- a/Resources/Prototypes/Entities/Objects/Devices/door_remote.yml +++ b/Resources/Prototypes/Entities/Objects/Devices/door_remote.yml @@ -11,8 +11,31 @@ 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]