]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Predict doors and airlocks (#25419)
authorNemanja <98561806+EmoGarbage404@users.noreply.github.com>
Thu, 22 Feb 2024 23:01:31 +0000 (18:01 -0500)
committerGitHub <noreply@github.com>
Thu, 22 Feb 2024 23:01:31 +0000 (10:01 +1100)
* predict doors and airlocks

* prying, too

* ack

* eek

25 files changed:
Content.Client/Doors/AirlockSystem.cs
Content.Client/Doors/DoorBoltSystem.cs [deleted file]
Content.Client/Doors/DoorSystem.cs
Content.Server/Administration/Systems/AdminVerbSystem.Tools.cs
Content.Server/DeviceLinking/Systems/DoorSignalControlSystem.cs
Content.Server/Doors/Systems/AirlockSystem.cs
Content.Server/Doors/Systems/DoorBoltSystem.cs [deleted file]
Content.Server/Doors/Systems/DoorSystem.cs
Content.Server/Doors/WireActions/DoorBoltLightWireAction.cs
Content.Server/Doors/WireActions/DoorBoltWireAction.cs
Content.Server/Magic/MagicSystem.cs
Content.Server/Remotes/DoorRemoteSystem.cs
Content.Server/Shuttles/Systems/DockingSystem.cs
Content.Server/Shuttles/Systems/ShuttleSystem.FasterThanLight.cs
Content.Server/Shuttles/Systems/ShuttleSystem.cs
Content.Server/Wires/WiresSystem.cs
Content.Shared/Doors/Components/AirlockComponent.cs
Content.Shared/Doors/Components/DoorBoltComponent.cs
Content.Shared/Doors/Components/DoorComponent.cs
Content.Shared/Doors/Systems/SharedAirlockSystem.cs
Content.Shared/Doors/Systems/SharedDoorBoltSystem.cs [deleted file]
Content.Shared/Doors/Systems/SharedDoorSystem.Bolts.cs [new file with mode: 0644]
Content.Shared/Doors/Systems/SharedDoorSystem.cs
Content.Shared/Prying/Systems/PryingSystem.cs
Content.Shared/Wires/SharedWiresSystem.cs

index 4837adf460b4827617114caf92e713f42927d5eb..58c873064334ee870e19ff57aa6c2dab3c4e5e66 100644 (file)
@@ -1,7 +1,6 @@
 using Content.Client.Wires.Visualizers;
 using Content.Shared.Doors.Components;
 using Content.Shared.Doors.Systems;
-using Content.Shared.Prying.Components;
 using Robust.Client.Animations;
 using Robust.Client.GameObjects;
 
@@ -16,13 +15,6 @@ public sealed class AirlockSystem : SharedAirlockSystem
         base.Initialize();
         SubscribeLocalEvent<AirlockComponent, ComponentStartup>(OnComponentStartup);
         SubscribeLocalEvent<AirlockComponent, AppearanceChangeEvent>(OnAppearanceChange);
-        SubscribeLocalEvent<AirlockComponent, BeforePryEvent>(OnAirlockPryAttempt);
-    }
-
-    private void OnAirlockPryAttempt(EntityUid uid, AirlockComponent component, ref BeforePryEvent args)
-    {
-        // TODO: Temporary until airlocks predicted.
-        args.Cancelled = true;
     }
 
     private void OnComponentStartup(EntityUid uid, AirlockComponent comp, ComponentStartup args)
@@ -104,7 +96,7 @@ public sealed class AirlockSystem : SharedAirlockSystem
                 ||  state == DoorState.Denying
                 || (state == DoorState.Open && comp.OpenUnlitVisible)
                 || (_appearanceSystem.TryGetData<bool>(uid, DoorVisuals.ClosedLights, out var closedLights, args.Component) && closedLights))
-                    && !boltedVisible && !emergencyLightsVisible; ;
+                    && !boltedVisible && !emergencyLightsVisible;
         }
 
         args.Sprite.LayerSetVisible(DoorVisualLayers.BaseUnlit, unlitVisible);
@@ -120,5 +112,17 @@ public sealed class AirlockSystem : SharedAirlockSystem
                 && !boltedVisible
             );
         }
+
+        switch (state)
+        {
+            case DoorState.Open:
+                args.Sprite.LayerSetState(DoorVisualLayers.BaseUnlit, comp.ClosingSpriteState);
+                args.Sprite.LayerSetAnimationTime(DoorVisualLayers.BaseUnlit, 0);
+                break;
+            case DoorState.Closed:
+                args.Sprite.LayerSetState(DoorVisualLayers.BaseUnlit, comp.OpeningSpriteState);
+                args.Sprite.LayerSetAnimationTime(DoorVisualLayers.BaseUnlit, 0);
+                break;
+        }
     }
 }
diff --git a/Content.Client/Doors/DoorBoltSystem.cs b/Content.Client/Doors/DoorBoltSystem.cs
deleted file mode 100644 (file)
index 58144cd..0000000
+++ /dev/null
@@ -1,12 +0,0 @@
-using Content.Client.Wires.Visualizers;
-using Content.Shared.Doors.Components;
-using Content.Shared.Doors.Systems;
-using Robust.Client.Animations;
-using Robust.Client.GameObjects;
-
-namespace Content.Client.Doors;
-
-public sealed class DoorBoltSystem : SharedDoorBoltSystem
-{
-    // Instantiate sub-class on client for prediction.
-}
index 80ce47e0c21a401e11c4d541dcbbf481eb557ef2..473ae97059aaa8149ede60828f0210d2d7aa8cd8 100644 (file)
@@ -3,8 +3,6 @@ using Content.Shared.Doors.Systems;
 using Robust.Client.Animations;
 using Robust.Client.GameObjects;
 using Robust.Client.ResourceManagement;
-using Robust.Shared.Audio;
-using Robust.Shared.Player;
 using Robust.Shared.Serialization.TypeSerializers.Implementations;
 using Robust.Shared.Timing;
 
@@ -73,7 +71,7 @@ public sealed class DoorSystem : SharedDoorSystem
 
     private void OnAppearanceChange(EntityUid uid, DoorComponent comp, ref AppearanceChangeEvent args)
     {
-        if (args.Sprite == null || !_gameTiming.IsFirstTimePredicted)
+        if (args.Sprite == null)
             return;
 
         if(!AppearanceSystem.TryGetData<DoorState>(uid, DoorVisuals.State, out var state, args.Component))
@@ -83,9 +81,9 @@ public sealed class DoorSystem : SharedDoorSystem
         {
             if (!_resourceCache.TryGetResource<RSIResource>(SpriteSpecifierSerializer.TextureRoot / baseRsi, out var res))
             {
-                Logger.Error("Unable to load RSI '{0}'. Trace:\n{1}", baseRsi, Environment.StackTrace);
+                Log.Error("Unable to load RSI '{0}'. Trace:\n{1}", baseRsi, Environment.StackTrace);
             }
-            foreach (ISpriteLayer layer in args.Sprite.AllLayers)
+            foreach (var layer in args.Sprite.AllLayers)
             {
                 layer.Rsi = res?.RSI;
             }
@@ -113,31 +111,24 @@ public sealed class DoorSystem : SharedDoorSystem
                 break;
             case DoorState.Opening:
                 if (animPlayer != null && comp.OpeningAnimationTime != 0.0)
-                    _animationSystem.Play(uid, animPlayer, (Animation)comp.OpeningAnimation, DoorComponent.AnimationKey);
+                    _animationSystem.Play((uid, animPlayer), (Animation)comp.OpeningAnimation, DoorComponent.AnimationKey);
                 break;
             case DoorState.Closing:
                 if (animPlayer != null && comp.ClosingAnimationTime != 0.0 && comp.CurrentlyCrushing.Count == 0)
-                    _animationSystem.Play(uid, animPlayer, (Animation)comp.ClosingAnimation, DoorComponent.AnimationKey);
+                    _animationSystem.Play((uid, animPlayer), (Animation)comp.ClosingAnimation, DoorComponent.AnimationKey);
                 break;
             case DoorState.Denying:
-                if (animPlayer != null && comp.DenyingAnimation != default)
-                    _animationSystem.Play(uid, animPlayer, (Animation)comp.DenyingAnimation, DoorComponent.AnimationKey);
+                if (animPlayer != null)
+                    _animationSystem.Play((uid, animPlayer), (Animation)comp.DenyingAnimation, DoorComponent.AnimationKey);
                 break;
             case DoorState.Welded:
                 break;
             case DoorState.Emagging:
-                if (animPlayer != null && comp.EmaggingAnimation != default)
-                    _animationSystem.Play(uid, animPlayer, (Animation)comp.EmaggingAnimation, DoorComponent.AnimationKey);
+                if (animPlayer != null)
+                    _animationSystem.Play((uid, animPlayer), (Animation)comp.EmaggingAnimation, DoorComponent.AnimationKey);
                 break;
             default:
                 throw new ArgumentOutOfRangeException($"Invalid door visual state {state}");
         }
     }
-
-    // TODO AUDIO PREDICT see comments in server-side PlaySound()
-    protected override void PlaySound(EntityUid uid, SoundSpecifier soundSpecifier, AudioParams audioParams, EntityUid? predictingPlayer, bool predicted)
-    {
-        if (GameTiming.InPrediction && GameTiming.IsFirstTimePredicted)
-            Audio.PlayEntity(soundSpecifier, Filter.Local(), uid, false, audioParams);
-    }
 }
index 296e48274c600c7699d61ede5416af37cb562741..c68336deab88d1bc9557989fe441ef2dd6f8d36b 100644 (file)
@@ -41,7 +41,7 @@ namespace Content.Server.Administration.Systems;
 
 public sealed partial class AdminVerbSystem
 {
-    [Dependency] private readonly DoorBoltSystem _boltsSystem = default!;
+    [Dependency] private readonly DoorSystem _door = default!;
     [Dependency] private readonly AirlockSystem _airlockSystem = default!;
     [Dependency] private readonly StackSystem _stackSystem = default!;
     [Dependency] private readonly SharedAccessSystem _accessSystem = default!;
@@ -78,7 +78,7 @@ public sealed partial class AdminVerbSystem
                         : new SpriteSpecifier.Texture(new("/Textures/Interface/AdminActions/bolt.png")),
                     Act = () =>
                     {
-                        _boltsSystem.SetBoltsWithAudio(args.Target, bolts, !bolts.BoltsDown);
+                        _door.SetBoltsDown((args.Target, bolts), !bolts.BoltsDown);
                     },
                     Impact = LogImpact.Medium,
                     Message = Loc.GetString(bolts.BoltsDown
index 1c0c9713cf112c17e419515edcfd1608f2761d36..fab9a306ae71852bfc99a9c7cca1647cc4b3d7ff 100644 (file)
@@ -12,7 +12,6 @@ namespace Content.Server.DeviceLinking.Systems
     [UsedImplicitly]
     public sealed class DoorSignalControlSystem : EntitySystem
     {
-        [Dependency] private readonly DoorBoltSystem _bolts = default!;
         [Dependency] private readonly DoorSystem _doorSystem = default!;
         [Dependency] private readonly DeviceLinkSystem _signalSystem = default!;
 
@@ -79,7 +78,7 @@ namespace Content.Server.DeviceLinking.Systems
                     bolt = state == SignalState.High;
                 }
 
-                _bolts.SetBoltsWithAudio(uid, bolts, bolt);
+                _doorSystem.SetBoltsDown((uid, bolts), bolt);
             }
         }
 
index 6d9cde8eb4e1b92973112f1ec16b02bd6102dd1d..71f9347e9ed36899eeb8af13df621ba0e68240f7 100644 (file)
@@ -1,13 +1,10 @@
 using Content.Server.DeviceLinking.Events;
 using Content.Server.Power.Components;
-using Content.Server.Power.EntitySystems;
 using Content.Server.Wires;
-using Content.Shared.Doors;
 using Content.Shared.Doors.Components;
 using Content.Shared.Doors.Systems;
 using Content.Shared.Interaction;
 using Content.Shared.Wires;
-using Content.Shared.Prying.Components;
 using Robust.Shared.Player;
 
 namespace Content.Server.Doors.Systems;
@@ -15,8 +12,6 @@ namespace Content.Server.Doors.Systems;
 public sealed class AirlockSystem : SharedAirlockSystem
 {
     [Dependency] private readonly WiresSystem _wiresSystem = default!;
-    [Dependency] private readonly PowerReceiverSystem _power = default!;
-    [Dependency] private readonly DoorBoltSystem _bolts = default!;
 
     public override void Initialize()
     {
@@ -26,13 +21,7 @@ public sealed class AirlockSystem : SharedAirlockSystem
         SubscribeLocalEvent<AirlockComponent, SignalReceivedEvent>(OnSignalReceived);
 
         SubscribeLocalEvent<AirlockComponent, PowerChangedEvent>(OnPowerChanged);
-        SubscribeLocalEvent<AirlockComponent, DoorStateChangedEvent>(OnStateChanged);
-        SubscribeLocalEvent<AirlockComponent, BeforeDoorOpenedEvent>(OnBeforeDoorOpened);
-        SubscribeLocalEvent<AirlockComponent, BeforeDoorDeniedEvent>(OnBeforeDoorDenied);
         SubscribeLocalEvent<AirlockComponent, ActivateInWorldEvent>(OnActivate, before: new[] { typeof(DoorSystem) });
-        SubscribeLocalEvent<AirlockComponent, GetPryTimeModifierEvent>(OnGetPryMod);
-        SubscribeLocalEvent<AirlockComponent, BeforePryEvent>(OnBeforePry);
-
     }
 
     private void OnAirlockInit(EntityUid uid, AirlockComponent component, ComponentInit args)
@@ -53,6 +42,9 @@ public sealed class AirlockSystem : SharedAirlockSystem
 
     private void OnPowerChanged(EntityUid uid, AirlockComponent component, ref PowerChangedEvent args)
     {
+        component.Powered = args.Powered;
+        Dirty(uid, component);
+
         if (TryComp<AppearanceComponent>(uid, out var appearanceComponent))
         {
             Appearance.SetData(uid, DoorVisuals.Powered, args.Powered, appearanceComponent);
@@ -73,80 +65,6 @@ public sealed class AirlockSystem : SharedAirlockSystem
         }
     }
 
-    private void OnStateChanged(EntityUid uid, AirlockComponent component, DoorStateChangedEvent args)
-    {
-        // TODO move to shared? having this be server-side, but having client-side door opening/closing & prediction
-        // means that sometimes the panels & bolt lights may be visible despite a door being completely open.
-
-        // Only show the maintenance panel if the airlock is closed
-        if (TryComp<WiresPanelComponent>(uid, out var wiresPanel))
-        {
-            _wiresSystem.ChangePanelVisibility(uid, wiresPanel, component.OpenPanelVisible || args.State != DoorState.Open);
-        }
-        // If the door is closed, we should look if the bolt was locked while closing
-        UpdateAutoClose(uid, component);
-
-        // Make sure the airlock auto closes again next time it is opened
-        if (args.State == DoorState.Closed)
-            component.AutoClose = true;
-    }
-
-    /// <summary>
-    /// Updates the auto close timer.
-    /// </summary>
-    public void UpdateAutoClose(EntityUid uid, AirlockComponent? airlock = null, DoorComponent? door = null)
-    {
-        if (!Resolve(uid, ref airlock, ref door))
-            return;
-
-        if (door.State != DoorState.Open)
-            return;
-
-        if (!airlock.AutoClose)
-            return;
-
-        if (!CanChangeState(uid, airlock))
-            return;
-
-        var autoev = new BeforeDoorAutoCloseEvent();
-        RaiseLocalEvent(uid, autoev, false);
-        if (autoev.Cancelled)
-            return;
-
-        DoorSystem.SetNextStateChange(uid, airlock.AutoCloseDelay * airlock.AutoCloseDelayModifier);
-    }
-
-    private void OnBeforeDoorOpened(EntityUid uid, AirlockComponent component, BeforeDoorOpenedEvent args)
-    {
-        if (!CanChangeState(uid, component))
-            args.Cancel();
-    }
-
-    protected override void OnBeforeDoorClosed(EntityUid uid, AirlockComponent component, BeforeDoorClosedEvent args)
-    {
-        base.OnBeforeDoorClosed(uid, component, args);
-
-        if (args.Cancelled)
-            return;
-
-        // only block based on bolts / power status when initially closing the door, not when its already
-        // mid-transition. Particularly relevant for when the door was pried-closed with a crowbar, which bypasses
-        // the initial power-check.
-
-        if (TryComp(uid, out DoorComponent? door)
-            && !door.Partial
-            && !CanChangeState(uid, component))
-        {
-            args.Cancel();
-        }
-    }
-
-    private void OnBeforeDoorDenied(EntityUid uid, AirlockComponent component, BeforeDoorDeniedEvent args)
-    {
-        if (!CanChangeState(uid, component))
-            args.Cancel();
-    }
-
     private void OnActivate(EntityUid uid, AirlockComponent component, ActivateInWorldEvent args)
     {
         if (TryComp<WiresPanelComponent>(uid, out var panel) &&
@@ -168,32 +86,4 @@ public sealed class AirlockSystem : SharedAirlockSystem
             component.AutoClose = false;
         }
     }
-
-    private void OnGetPryMod(EntityUid uid, AirlockComponent component, ref GetPryTimeModifierEvent args)
-    {
-        if (_power.IsPowered(uid))
-            args.PryTimeModifier *= component.PoweredPryModifier;
-
-        if (_bolts.IsBolted(uid))
-            args.PryTimeModifier *= component.BoltedPryModifier;
-    }
-
-    private void OnBeforePry(EntityUid uid, AirlockComponent component, ref BeforePryEvent args)
-    {
-        if (args.Cancelled)
-            return;
-
-        if (!this.IsPowered(uid, EntityManager) || args.PryPowered)
-            return;
-
-        args.Message = "airlock-component-cannot-pry-is-powered-message";
-
-        args.Cancelled = true;
-
-    }
-
-    public bool CanChangeState(EntityUid uid, AirlockComponent component)
-    {
-        return this.IsPowered(uid, EntityManager) && !_bolts.IsBolted(uid);
-    }
 }
diff --git a/Content.Server/Doors/Systems/DoorBoltSystem.cs b/Content.Server/Doors/Systems/DoorBoltSystem.cs
deleted file mode 100644 (file)
index 133af00..0000000
+++ /dev/null
@@ -1,90 +0,0 @@
-using Content.Server.Power.Components;
-using Content.Server.Power.EntitySystems;
-using Content.Shared.Doors;
-using Content.Shared.Doors.Components;
-using Content.Shared.Doors.Systems;
-
-namespace Content.Server.Doors.Systems;
-
-public sealed class DoorBoltSystem : SharedDoorBoltSystem
-{
-
-    public override void Initialize()
-    {
-        base.Initialize();
-
-        SubscribeLocalEvent<DoorBoltComponent, PowerChangedEvent>(OnPowerChanged);
-        SubscribeLocalEvent<DoorBoltComponent, DoorStateChangedEvent>(OnStateChanged);
-    }
-
-    private void OnPowerChanged(EntityUid uid, DoorBoltComponent component, ref PowerChangedEvent args)
-    {
-        if (args.Powered)
-        {
-            if (component.BoltWireCut)
-                SetBoltsWithAudio(uid, component, true);
-        }
-
-        UpdateBoltLightStatus(uid, component);
-    }
-
-    public void UpdateBoltLightStatus(EntityUid uid, DoorBoltComponent component)
-    {
-        if (!TryComp<AppearanceComponent>(uid, out var appearance))
-            return;
-
-        Appearance.SetData(uid, DoorVisuals.BoltLights, GetBoltLightsVisible(uid, component), appearance);
-    }
-
-    public bool GetBoltLightsVisible(EntityUid uid, DoorBoltComponent component)
-    {
-        return component.BoltLightsEnabled &&
-               component.BoltsDown &&
-               this.IsPowered(uid, EntityManager);
-    }
-
-    public void SetBoltLightsEnabled(EntityUid uid, DoorBoltComponent component, bool value)
-    {
-        if (component.BoltLightsEnabled == value)
-            return;
-
-        component.BoltLightsEnabled = value;
-        UpdateBoltLightStatus(uid, component);
-    }
-
-    public void SetBoltsDown(EntityUid uid, DoorBoltComponent component, bool value)
-    {
-        if (component.BoltsDown == value)
-            return;
-
-        component.BoltsDown = value;
-        UpdateBoltLightStatus(uid, component);
-    }
-
-    private void OnStateChanged(EntityUid uid, DoorBoltComponent component, DoorStateChangedEvent args)
-    {
-        // If the door is closed, we should look if the bolt was locked while closing
-        UpdateBoltLightStatus(uid, component);
-    }
-
-    public void SetBoltsWithAudio(EntityUid uid, DoorBoltComponent component, bool newBolts)
-    {
-        if (newBolts == component.BoltsDown)
-            return;
-
-        component.BoltsDown = newBolts;
-        Audio.PlayPvs(newBolts ? component.BoltDownSound : component.BoltUpSound, uid);
-        UpdateBoltLightStatus(uid, component);
-    }
-
-    public bool IsBolted(EntityUid uid, DoorBoltComponent? component = null)
-    {
-        if (!Resolve(uid, ref component))
-        {
-            return false;
-        }
-
-        return component.BoltsDown;
-    }
-}
-
index d59ea5962295b8a39688cd8e8101b339b31e0c48..5968e445c19a8df747bd089fe6523f89aefe2c84 100644 (file)
@@ -1,49 +1,22 @@
 using Content.Server.Access;
-using Content.Server.Administration.Logs;
 using Content.Server.Atmos.Components;
 using Content.Server.Atmos.EntitySystems;
-using Content.Server.Power.EntitySystems;
-using Content.Shared.Database;
+using Content.Server.Power.Components;
 using Content.Shared.Doors.Components;
 using Content.Shared.Doors.Systems;
-using Content.Shared.Emag.Systems;
-using Content.Shared.Interaction;
-using Content.Shared.Prying.Components;
-using Content.Shared.Prying.Systems;
-using Content.Shared.Tools.Systems;
-using Robust.Shared.Audio;
 using Robust.Shared.Physics.Components;
-using Robust.Shared.Physics.Events;
 
 namespace Content.Server.Doors.Systems;
 
 public sealed class DoorSystem : SharedDoorSystem
 {
-    [Dependency] private readonly IAdminLogManager _adminLog = default!;
-    [Dependency] private readonly DoorBoltSystem _bolts = default!;
     [Dependency] private readonly AirtightSystem _airtightSystem = default!;
-    [Dependency] private readonly PryingSystem _pryingSystem = default!;
 
     public override void Initialize()
     {
         base.Initialize();
 
-        SubscribeLocalEvent<DoorComponent, WeldableAttemptEvent>(OnWeldAttempt);
-        SubscribeLocalEvent<DoorComponent, WeldableChangedEvent>(OnWeldChanged);
-        SubscribeLocalEvent<DoorComponent, GotEmaggedEvent>(OnEmagged);
-        SubscribeLocalEvent<DoorComponent, PriedEvent>(OnAfterPry);
-    }
-
-    protected override void OnActivate(EntityUid uid, DoorComponent door, ActivateInWorldEvent args)
-    {
-        // TODO once access permissions are shared, move this back to shared.
-        if (args.Handled || !door.ClickOpen)
-            return;
-
-        if (!TryToggleDoor(uid, door, args.User))
-            _pryingSystem.TryPry(uid, args.User, out _);
-
-        args.Handled = true;
+        SubscribeLocalEvent<DoorBoltComponent, PowerChangedEvent>(OnBoltPowerChanged);
     }
 
     protected override void SetCollidable(
@@ -65,144 +38,16 @@ public sealed class DoorSystem : SharedDoorSystem
         base.SetCollidable(uid, collidable, door, physics, occluder);
     }
 
-    // TODO AUDIO PREDICT Figure out a better way to handle sound and prediction. For now, this works well enough?
-    //
-    // Currently a client will predict when a door is going to close automatically. So any client in PVS range can just
-    // play their audio locally. Playing it server-side causes an odd delay, while in shared it causes double-audio.
-    //
-    // But if we just do that, then if a door is closed prematurely as the result of an interaction (i.e., using "E" on
-    // an open door), then the audio would only be played for the client performing the interaction.
-    //
-    // So we do this:
-    // - Play audio client-side IF the closing is being predicted (auto-close or predicted interaction)
-    // - Server assumes automated closing is predicted by clients and does not play audio unless otherwise specified.
-    // - Major exception is player interactions, which other players cannot predict
-    // - In that case, send audio to all players, except possibly the interacting player if it was a predicted
-    //   interaction.
-
-    /// <summary>
-    /// Selectively send sound to clients, taking care to not send the double-audio.
-    /// </summary>
-    /// <param name="uid">The audio source</param>
-    /// <param name="soundSpecifier">The sound</param>
-    /// <param name="audioParams">The audio parameters.</param>
-    /// <param name="predictingPlayer">The user (if any) that instigated an interaction</param>
-    /// <param name="predicted">Whether this interaction would have been predicted. If the predicting player is null,
-    /// this assumes it would have been predicted by all players in PVS range.</param>
-    protected override void PlaySound(EntityUid uid, SoundSpecifier soundSpecifier, AudioParams audioParams, EntityUid? predictingPlayer, bool predicted)
-    {
-        // If this sound would have been predicted by all clients, do not play any audio.
-        if (predicted && predictingPlayer == null)
-            return;
-
-        if (predicted)
-            Audio.PlayPredicted(soundSpecifier, uid, predictingPlayer, audioParams);
-        else
-            Audio.PlayPvs(soundSpecifier, uid, audioParams);
-    }
-
-    #region DoAfters
-    private void OnWeldAttempt(EntityUid uid, DoorComponent component, WeldableAttemptEvent args)
-    {
-        if (component.CurrentlyCrushing.Count > 0)
-        {
-            args.Cancel();
-            return;
-        }
-        if (component.State != DoorState.Closed && component.State != DoorState.Welded)
-        {
-            args.Cancel();
-        }
-    }
-
-    private void OnWeldChanged(EntityUid uid, DoorComponent component, ref WeldableChangedEvent args)
-    {
-        if (component.State == DoorState.Closed)
-            SetState(uid, DoorState.Welded, component);
-        else if (component.State == DoorState.Welded)
-            SetState(uid, DoorState.Closed, component);
-    }
-    #endregion
-
-
-    /// <summary>
-    ///     Open a door if a player or door-bumper (PDA, ID-card) collide with the door. Sadly, bullets no longer
-    ///     generate "access denied" sounds as you fire at a door.
-    /// </summary>
-    protected override void HandleCollide(EntityUid uid, DoorComponent door, ref StartCollideEvent args)
-    {
-        // TODO ACCESS READER move access reader to shared and predict door opening/closing
-        // Then this can be moved to the shared system without mispredicting.
-        if (!door.BumpOpen)
-            return;
-
-        if (door.State is not (DoorState.Closed or DoorState.Denying))
-            return;
-
-        var otherUid = args.OtherEntity;
-
-        if (Tags.HasTag(otherUid, "DoorBumpOpener"))
-            TryOpen(uid, door, otherUid, quiet: door.State == DoorState.Denying);
-    }
-    private void OnEmagged(EntityUid uid, DoorComponent door, ref GotEmaggedEvent args)
-    {
-        if (TryComp<AirlockComponent>(uid, out var airlockComponent))
-        {
-            if (_bolts.IsBolted(uid) || !this.IsPowered(uid, EntityManager))
-                return;
-
-            if (door.State == DoorState.Closed)
-            {
-                SetState(uid, DoorState.Emagging, door);
-                PlaySound(uid, door.SparkSound, AudioParams.Default.WithVolume(8), args.UserUid, false);
-                args.Handled = true;
-            }
-        }
-    }
-
-    public override void StartOpening(EntityUid uid, DoorComponent? door = null, EntityUid? user = null, bool predicted = false)
-    {
-        if (!Resolve(uid, ref door))
-            return;
-
-        var lastState = door.State;
-
-        SetState(uid, DoorState.Opening, door);
-
-        if (door.OpenSound != null)
-            PlaySound(uid, door.OpenSound, AudioParams.Default.WithVolume(-5), user, predicted);
-
-        if (lastState == DoorState.Emagging && TryComp<DoorBoltComponent>(uid, out var doorBoltComponent))
-            _bolts.SetBoltsWithAudio(uid, doorBoltComponent, !doorBoltComponent.BoltsDown);
-    }
-
-    /// <summary>
-    ///     Open or close a door after it has been successfuly pried.
-    /// </summary>
-    private void OnAfterPry(EntityUid uid, DoorComponent door, ref PriedEvent args)
+    private void OnBoltPowerChanged(Entity<DoorBoltComponent> ent, ref PowerChangedEvent args)
     {
-        if (door.State == DoorState.Closed)
+        if (args.Powered)
         {
-            _adminLog.Add(LogType.Action, LogImpact.Medium, $"{ToPrettyString(args.User)} pried {ToPrettyString(uid)} open");
-            StartOpening(uid, door, args.User);
+            if (ent.Comp.BoltWireCut)
+                SetBoltsDown(ent, true);
         }
-        else if (door.State == DoorState.Open)
-        {
-            _adminLog.Add(LogType.Action, LogImpact.Medium, $"{ToPrettyString(args.User)} pried {ToPrettyString(uid)} closed");
-            StartClosing(uid, door, args.User);
-        }
-    }
 
-    protected override void CheckDoorBump(Entity<DoorComponent, PhysicsComponent> ent)
-    {
-        var (uid, door, physics) = ent;
-        if (door.BumpOpen)
-        {
-            foreach (var other in PhysicsSystem.GetContactingEntities(uid, physics, approximate: true))
-            {
-                if (Tags.HasTag(other, "DoorBumpOpener") && TryOpen(uid, door, other, quiet: true))
-                    break;
-            }
-        }
+        UpdateBoltLightStatus(ent);
+        ent.Comp.Powered = args.Powered;
+        Dirty(ent, ent.Comp);
     }
 }
index 233ebfed3face3ded209af6d70ef1db22d4c60b1..c5e3aa80c9eb6c9080d533a323bac3d65fb50e38 100644 (file)
@@ -18,19 +18,18 @@ public sealed partial class DoorBoltLightWireAction : ComponentWireAction<DoorBo
 
     public override bool Cut(EntityUid user, Wire wire, DoorBoltComponent door)
     {
-        EntityManager.System<DoorBoltSystem>().SetBoltLightsEnabled(wire.Owner, door, false);
+        EntityManager.System<DoorSystem>().SetBoltLightsEnabled((wire.Owner, door), false);
         return true;
     }
 
     public override bool Mend(EntityUid user, Wire wire, DoorBoltComponent door)
     {
-
-        EntityManager.System<DoorBoltSystem>().SetBoltLightsEnabled(wire.Owner, door, true);
+        EntityManager.System<DoorSystem>().SetBoltLightsEnabled((wire.Owner, door), true);
         return true;
     }
 
     public override void Pulse(EntityUid user, Wire wire, DoorBoltComponent door)
     {
-        EntityManager.System<DoorBoltSystem>().SetBoltLightsEnabled(wire.Owner, door, !door.BoltLightsEnabled);
+        EntityManager.System<DoorSystem>().SetBoltLightsEnabled((wire.Owner, door), !door.BoltLightsEnabled);
     }
 }
index 29b5d726a285f256f7fc9f9b3ad5ea25cd28089c..fc1cf50cd87d5c6dff4b6f7b0e9fa839afd949e5 100644 (file)
@@ -19,24 +19,24 @@ public sealed partial class DoorBoltWireAction : ComponentWireAction<DoorBoltCom
 
     public override bool Cut(EntityUid user, Wire wire, DoorBoltComponent airlock)
     {
-        EntityManager.System<DoorBoltSystem>().SetBoltWireCut(airlock, true);
+        EntityManager.System<DoorSystem>().SetBoltWireCut((wire.Owner, airlock), true);
         if (!airlock.BoltsDown && IsPowered(wire.Owner))
-            EntityManager.System<DoorBoltSystem>().SetBoltsWithAudio(wire.Owner, airlock, true);
+            EntityManager.System<DoorSystem>().SetBoltsDown((wire.Owner, airlock), true, user);
 
         return true;
     }
 
     public override bool Mend(EntityUid user, Wire wire, DoorBoltComponent door)
     {
-        EntityManager.System<DoorBoltSystem>().SetBoltWireCut(door, false);
+        EntityManager.System<DoorSystem>().SetBoltWireCut((wire.Owner, door), false);
         return true;
     }
 
     public override void Pulse(EntityUid user, Wire wire, DoorBoltComponent door)
     {
         if (IsPowered(wire.Owner))
-            EntityManager.System<DoorBoltSystem>().SetBoltsWithAudio(wire.Owner, door, !door.BoltsDown);
+            EntityManager.System<DoorSystem>().SetBoltsDown((wire.Owner, door), !door.BoltsDown);
         else if (!door.BoltsDown)
-            EntityManager.System<DoorBoltSystem>().SetBoltsWithAudio(wire.Owner, door, true);
+            EntityManager.System<DoorSystem>().SetBoltsDown((wire.Owner, door), true);
     }
 }
index d8febe0d1ff2ee7c344aa918b4bbb3cf54a20f2f..2c9b11dbd3dc76e253192c084e35776eabfc6895 100644 (file)
@@ -36,7 +36,6 @@ public sealed class MagicSystem : EntitySystem
     [Dependency] private readonly IComponentFactory _compFact = default!;
     [Dependency] private readonly IMapManager _mapManager = default!;
     [Dependency] private readonly IRobustRandom _random = default!;
-    [Dependency] private readonly DoorBoltSystem _boltsSystem = default!;
     [Dependency] private readonly BodySystem _bodySystem = default!;
     [Dependency] private readonly EntityLookupSystem _lookup = default!;
     [Dependency] private readonly SharedDoorSystem _doorSystem = default!;
@@ -307,7 +306,7 @@ public sealed class MagicSystem : EntitySystem
         foreach (var entity in _lookup.GetEntitiesInRange(coords, args.Range))
         {
             if (TryComp<DoorBoltComponent>(entity, out var bolts))
-                _boltsSystem.SetBoltsDown(entity, bolts, false);
+                _doorSystem.SetBoltsDown((entity, bolts), false);
 
             if (TryComp<DoorComponent>(entity, out var doorComp) && doorComp.State is not DoorState.Open)
                 _doorSystem.StartOpening(entity);
index a88508ce53d5994ca8d6d74a38f357748acd9edb..9be7e5e96b41452428ed65ce2f82b8448531c574 100644 (file)
@@ -18,7 +18,6 @@ namespace Content.Server.Remotes
     public sealed class DoorRemoteSystem : EntitySystem
     {
         [Dependency] private readonly IAdminLogManager _adminLogger = default!;
-        [Dependency] private readonly DoorBoltSystem _bolts = default!;
         [Dependency] private readonly AirlockSystem _airlock = default!;
         [Dependency] private readonly SharedPopupSystem _popupSystem = default!;
         [Dependency] private readonly DoorSystem _doorSystem = default!;
@@ -105,7 +104,7 @@ namespace Content.Server.Remotes
                     {
                         if (!boltsComp.BoltWireCut)
                         {
-                            _bolts.SetBoltsWithAudio(args.Target.Value, boltsComp, !boltsComp.BoltsDown);
+                            _doorSystem.SetBoltsDown((args.Target.Value, boltsComp), !boltsComp.BoltsDown, args.User);
                             _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");
                         }
                     }
index 778d24437609a24f6893108a9e397f3c092b4d83..7f698850450edf01fef841463516ed365359b160 100644 (file)
@@ -20,7 +20,6 @@ namespace Content.Server.Shuttles.Systems
     public sealed partial class DockingSystem : EntitySystem
     {
         [Dependency] private readonly IMapManager _mapManager = default!;
-        [Dependency] private readonly DoorBoltSystem _bolts = default!;
         [Dependency] private readonly DoorSystem _doorSystem = default!;
         [Dependency] private readonly FixtureSystem _fixtureSystem = default!;
         [Dependency] private readonly PathfindingSystem _pathfinding = default!;
@@ -363,7 +362,7 @@ namespace Content.Server.Shuttles.Systems
                     doorA.ChangeAirtight = false;
                     if (TryComp<DoorBoltComponent>(dockAUid, out var airlockA))
                     {
-                        _bolts.SetBoltsWithAudio(dockAUid, airlockA, true);
+                        _doorSystem.SetBoltsDown((dockAUid, airlockA), true);
                     }
                 }
             }
@@ -375,7 +374,7 @@ namespace Content.Server.Shuttles.Systems
                     doorB.ChangeAirtight = false;
                     if (TryComp<DoorBoltComponent>(dockBUid, out var airlockB))
                     {
-                        _bolts.SetBoltsWithAudio(dockBUid, airlockB, true);
+                        _doorSystem.SetBoltsDown((dockBUid, airlockB), true);
                     }
                 }
             }
@@ -470,7 +469,7 @@ namespace Content.Server.Shuttles.Systems
                 return;
 
             if (TryComp<DoorBoltComponent>(dockUid, out var airlock))
-                _bolts.SetBoltsWithAudio(dockUid, airlock, false);
+                _doorSystem.SetBoltsDown((dockUid, airlock), false);
 
             if (TryComp(dockUid, out DoorComponent? door) && _doorSystem.TryClose(dockUid, door))
                 door.ChangeAirtight = true;
index 99ce4a044d6dbd46ba9936a4bada4d321f3cf642..d976b634d5bda94ab710fd9263dda5b304eebbf1 100644 (file)
@@ -459,7 +459,7 @@ public sealed partial class ShuttleSystem
                 continue;
 
             _doors.TryClose(doorUid);
-            _bolts.SetBoltsWithAudio(doorUid, door, enabled);
+            _doors.SetBoltsDown((doorUid, door), enabled);
         }
     }
 
index 471b46e507d7dfa53932f699567c5c2604885270..5f11ce25a4b5286cfd38cc6c51437e74e921b75a 100644 (file)
@@ -34,7 +34,6 @@ public sealed partial class ShuttleSystem : SharedShuttleSystem
     [Dependency] private readonly BodySystem _bobby = default!;
     [Dependency] private readonly DockingSystem _dockSystem = default!;
     [Dependency] private readonly DoorSystem _doors = default!;
-    [Dependency] private readonly DoorBoltSystem _bolts = default!;
     [Dependency] private readonly EntityLookupSystem _lookup = default!;
     [Dependency] private readonly FixtureSystem _fixtures = default!;
     [Dependency] private readonly MapLoaderSystem _loader = default!;
index 2b23b09360342f2bbba70e1215cc49f6b94f569f..0f9ee8b0f75fd71ef79416f75d1e2db1ef0343e4 100644 (file)
@@ -620,13 +620,6 @@ public sealed class WiresSystem : SharedWiresSystem
         }
     }
 
-    public void ChangePanelVisibility(EntityUid uid, WiresPanelComponent component, bool visible)
-    {
-        component.Visible = visible;
-        UpdateAppearance(uid, component);
-        Dirty(uid, component);
-    }
-
     public void SetWiresPanelSecurity(EntityUid uid, WiresPanelSecurityComponent component, WiresPanelSecurityEvent args)
     {
         component.Examine = args.Examine;
index 332df2ac77d26502ed53c5be18e79a8e7e73c53e..dd8241c64ec7de3fa431ab91ece1492f80bf1c8d 100644 (file)
@@ -12,6 +12,9 @@ namespace Content.Shared.Doors.Components;
 [Access(typeof(SharedAirlockSystem), Friend = AccessPermissions.ReadWriteExecute, Other = AccessPermissions.Read)]
 public sealed partial class AirlockComponent : Component
 {
+    [DataField, AutoNetworkedField]
+    public bool Powered;
+
     // Need to network airlock safety state to avoid mis-predicts when a door auto-closes as the client walks through the door.
     [ViewVariables(VVAccess.ReadWrite)]
     [DataField, AutoNetworkedField]
index 52f7d56db554fb0be680b87db59147365132da3f..a5b0621ff4256a3054ce1a777ee14f0d2bcb7773 100644 (file)
@@ -8,36 +8,43 @@ namespace Content.Shared.Doors.Components;
 /// Companion component to DoorComponent that handles bolt-specific behavior.
 /// </summary>
 [RegisterComponent, NetworkedComponent]
-[Access(typeof(SharedDoorBoltSystem))]
+[Access(typeof(SharedDoorSystem))]
+[AutoGenerateComponentState]
 public sealed partial class DoorBoltComponent : Component
 {
     /// <summary>
     /// Sound to play when the bolts on the airlock go up.
     /// </summary>
-    [DataField("boltUpSound"), ViewVariables(VVAccess.ReadWrite)]
+    [DataField, ViewVariables(VVAccess.ReadWrite)]
     public SoundSpecifier BoltUpSound = new SoundPathSpecifier("/Audio/Machines/boltsup.ogg");
 
     /// <summary>
     /// Sound to play when the bolts on the airlock go down.
     /// </summary>
-    [DataField("boltDownSound"), ViewVariables(VVAccess.ReadWrite)]
+    [DataField, ViewVariables(VVAccess.ReadWrite)]
     public SoundSpecifier BoltDownSound = new SoundPathSpecifier("/Audio/Machines/boltsdown.ogg");
 
     /// <summary>
     /// Whether the door bolts are currently deployed.
     /// </summary>
-    [ViewVariables(VVAccess.ReadWrite)]
+    [DataField, AutoNetworkedField]
     public bool BoltsDown;
 
     /// <summary>
     /// Whether the bolt lights are currently enabled.
     /// </summary>
-    [ViewVariables(VVAccess.ReadWrite)]
+    [DataField, AutoNetworkedField]
     public bool BoltLightsEnabled = true;
 
     /// <summary>
     /// True if the bolt wire is cut, which will force the airlock to always be bolted as long as it has power.
     /// </summary>
-    [ViewVariables(VVAccess.ReadWrite)]
+    [DataField, AutoNetworkedField]
     public bool BoltWireCut;
+
+    /// <summary>
+    /// Used for prediction. true if the door has power.
+    /// </summary>
+    [DataField, AutoNetworkedField]
+    public bool Powered;
 }
index 135f8b0856d9af368c5a1cd4e263668136ed7d41..d1b93d845d7340649bc3bbd0d209074392892f7e 100644 (file)
@@ -124,14 +124,14 @@ public sealed partial class DoorComponent : Component
     /// If false, this door is incapable of crushing entities. This just determines whether it will apply damage and
     /// stun, not whether it can close despite entities being in the way.
     /// </summary>
-    [DataField]
+    [DataField, AutoNetworkedField]
     public bool CanCrush = true;
 
     /// <summary>
     /// Whether to check for colliding entities before closing. This may be overridden by other system by subscribing to
     /// <see cref="BeforeDoorClosedEvent"/>. For example, hacked airlocks will set this to false.
     /// </summary>
-    [DataField]
+    [DataField, AutoNetworkedField]
     public bool PerformCollisionCheck = true;
 
     /// <summary>
@@ -190,7 +190,7 @@ public sealed partial class DoorComponent : Component
     /// The sprite state used for the door when it's being emagged.
     /// </summary>
     [DataField]
-    public string EmaggingSpriteState = "emagging";
+    public string EmaggingSpriteState = "sparks";
 
     /// <summary>
     /// The sprite state used for the door when it's open.
index 9e1273b7843fc57a1d4ccfdd6520d950b4ef2d26..a3172bb8c3ac28a84c40a32271742d77b82eef56 100644 (file)
@@ -1,5 +1,7 @@
 using Content.Shared.Doors.Components;
 using Content.Shared.Popups;
+using Content.Shared.Prying.Components;
+using Content.Shared.Wires;
 
 namespace Content.Shared.Doors.Systems;
 
@@ -8,18 +10,112 @@ public abstract class SharedAirlockSystem : EntitySystem
     [Dependency] protected readonly SharedAppearanceSystem Appearance = default!;
     [Dependency] protected readonly SharedDoorSystem DoorSystem = default!;
     [Dependency] protected readonly SharedPopupSystem Popup = default!;
+    [Dependency] private readonly SharedWiresSystem _wiresSystem = default!;
 
     public override void Initialize()
     {
         base.Initialize();
 
         SubscribeLocalEvent<AirlockComponent, BeforeDoorClosedEvent>(OnBeforeDoorClosed);
+        SubscribeLocalEvent<AirlockComponent, DoorStateChangedEvent>(OnStateChanged);
+        SubscribeLocalEvent<AirlockComponent, BeforeDoorOpenedEvent>(OnBeforeDoorOpened);
+        SubscribeLocalEvent<AirlockComponent, BeforeDoorDeniedEvent>(OnBeforeDoorDenied);
+        SubscribeLocalEvent<AirlockComponent, GetPryTimeModifierEvent>(OnGetPryMod);
+        SubscribeLocalEvent<AirlockComponent, BeforePryEvent>(OnBeforePry);
     }
 
-    protected virtual void OnBeforeDoorClosed(EntityUid uid, AirlockComponent airlock, BeforeDoorClosedEvent args)
+    private void OnBeforeDoorClosed(EntityUid uid, AirlockComponent airlock, BeforeDoorClosedEvent args)
     {
+        if (args.Cancelled)
+            return;
+
         if (!airlock.Safety)
             args.PerformCollisionCheck = false;
+
+        // only block based on bolts / power status when initially closing the door, not when its already
+        // mid-transition. Particularly relevant for when the door was pried-closed with a crowbar, which bypasses
+        // the initial power-check.
+
+        if (TryComp(uid, out DoorComponent? door)
+            && !door.Partial
+            && !CanChangeState(uid, airlock))
+        {
+            args.Cancel();
+        }
+    }
+
+    private void OnStateChanged(EntityUid uid, AirlockComponent component, DoorStateChangedEvent args)
+    {
+        // Only show the maintenance panel if the airlock is closed
+        if (TryComp<WiresPanelComponent>(uid, out var wiresPanel))
+        {
+            _wiresSystem.ChangePanelVisibility(uid, wiresPanel, component.OpenPanelVisible || args.State != DoorState.Open);
+        }
+        // If the door is closed, we should look if the bolt was locked while closing
+        UpdateAutoClose(uid, component);
+
+        // Make sure the airlock auto closes again next time it is opened
+        if (args.State == DoorState.Closed)
+            component.AutoClose = true;
+    }
+
+    private void OnBeforeDoorOpened(EntityUid uid, AirlockComponent component, BeforeDoorOpenedEvent args)
+    {
+        if (!CanChangeState(uid, component))
+            args.Cancel();
+    }
+
+    private void OnBeforeDoorDenied(EntityUid uid, AirlockComponent component, BeforeDoorDeniedEvent args)
+    {
+        if (!CanChangeState(uid, component))
+            args.Cancel();
+    }
+
+    private void OnGetPryMod(EntityUid uid, AirlockComponent component, ref GetPryTimeModifierEvent args)
+    {
+        if (component.Powered)
+            args.PryTimeModifier *= component.PoweredPryModifier;
+
+        if (DoorSystem.IsBolted(uid))
+            args.PryTimeModifier *= component.BoltedPryModifier;
+    }
+
+    /// <summary>
+    /// Updates the auto close timer.
+    /// </summary>
+    public void UpdateAutoClose(EntityUid uid, AirlockComponent? airlock = null, DoorComponent? door = null)
+    {
+        if (!Resolve(uid, ref airlock, ref door))
+            return;
+
+        if (door.State != DoorState.Open)
+            return;
+
+        if (!airlock.AutoClose)
+            return;
+
+        if (!CanChangeState(uid, airlock))
+            return;
+
+        var autoev = new BeforeDoorAutoCloseEvent();
+        RaiseLocalEvent(uid, autoev);
+        if (autoev.Cancelled)
+            return;
+
+        DoorSystem.SetNextStateChange(uid, airlock.AutoCloseDelay * airlock.AutoCloseDelayModifier);
+    }
+
+    private void OnBeforePry(EntityUid uid, AirlockComponent component, ref BeforePryEvent args)
+    {
+        if (args.Cancelled)
+            return;
+
+        if (!component.Powered || args.PryPowered)
+            return;
+
+        args.Message = "airlock-component-cannot-pry-is-powered-message";
+
+        args.Cancelled = true;
     }
 
     public void UpdateEmergencyLightStatus(EntityUid uid, AirlockComponent component)
@@ -45,4 +141,9 @@ public abstract class SharedAirlockSystem : EntitySystem
     {
         component.Safety = value;
     }
+
+    public bool CanChangeState(EntityUid uid, AirlockComponent component)
+    {
+        return component.Powered && !DoorSystem.IsBolted(uid);
+    }
 }
diff --git a/Content.Shared/Doors/Systems/SharedDoorBoltSystem.cs b/Content.Shared/Doors/Systems/SharedDoorBoltSystem.cs
deleted file mode 100644 (file)
index 7c5ef45..0000000
+++ /dev/null
@@ -1,61 +0,0 @@
-using Content.Shared.Doors.Components;
-using Content.Shared.Popups;
-using Content.Shared.Prying.Components;
-using Robust.Shared.Audio;
-using Robust.Shared.Audio.Systems;
-
-namespace Content.Shared.Doors.Systems;
-
-public abstract class SharedDoorBoltSystem : EntitySystem
-{
-
-    [Dependency] protected readonly SharedAppearanceSystem Appearance = default!;
-    [Dependency] protected readonly SharedAudioSystem Audio = default!;
-    [Dependency] protected readonly SharedPopupSystem Popup = default!;
-    public override void Initialize()
-    {
-        base.Initialize();
-
-        SubscribeLocalEvent<DoorBoltComponent, BeforeDoorOpenedEvent>(OnBeforeDoorOpened);
-        SubscribeLocalEvent<DoorBoltComponent, BeforeDoorClosedEvent>(OnBeforeDoorClosed);
-        SubscribeLocalEvent<DoorBoltComponent, BeforeDoorDeniedEvent>(OnBeforeDoorDenied);
-        SubscribeLocalEvent<DoorBoltComponent, BeforePryEvent>(OnDoorPry);
-
-    }
-
-    private void OnDoorPry(EntityUid uid, DoorBoltComponent component, ref BeforePryEvent args)
-    {
-        if (args.Cancelled)
-            return;
-
-        if (!component.BoltsDown || args.Force)
-            return;
-
-        args.Message = "airlock-component-cannot-pry-is-bolted-message";
-
-        args.Cancelled = true;
-    }
-
-    private void OnBeforeDoorOpened(EntityUid uid, DoorBoltComponent component, BeforeDoorOpenedEvent args)
-    {
-        if (component.BoltsDown)
-            args.Cancel();
-    }
-
-    private void OnBeforeDoorClosed(EntityUid uid, DoorBoltComponent component, BeforeDoorClosedEvent args)
-    {
-        if (component.BoltsDown)
-            args.Cancel();
-    }
-
-    private void OnBeforeDoorDenied(EntityUid uid, DoorBoltComponent component, BeforeDoorDeniedEvent args)
-    {
-        if (component.BoltsDown)
-            args.Cancel();
-    }
-
-    public void SetBoltWireCut(DoorBoltComponent component, bool value)
-    {
-        component.BoltWireCut = value;
-    }
-}
diff --git a/Content.Shared/Doors/Systems/SharedDoorSystem.Bolts.cs b/Content.Shared/Doors/Systems/SharedDoorSystem.Bolts.cs
new file mode 100644 (file)
index 0000000..35681bf
--- /dev/null
@@ -0,0 +1,109 @@
+using Content.Shared.Doors.Components;
+using Content.Shared.Prying.Components;
+
+namespace Content.Shared.Doors.Systems;
+
+public abstract partial class SharedDoorSystem
+{
+    public void InitializeBolts()
+    {
+        base.Initialize();
+
+        SubscribeLocalEvent<DoorBoltComponent, BeforeDoorOpenedEvent>(OnBeforeDoorOpened);
+        SubscribeLocalEvent<DoorBoltComponent, BeforeDoorClosedEvent>(OnBeforeDoorClosed);
+        SubscribeLocalEvent<DoorBoltComponent, BeforeDoorDeniedEvent>(OnBeforeDoorDenied);
+        SubscribeLocalEvent<DoorBoltComponent, BeforePryEvent>(OnDoorPry);
+        SubscribeLocalEvent<DoorBoltComponent, DoorStateChangedEvent>(OnStateChanged);
+    }
+
+    private void OnDoorPry(EntityUid uid, DoorBoltComponent component, ref BeforePryEvent args)
+    {
+        if (args.Cancelled)
+            return;
+
+        if (!component.BoltsDown || args.Force)
+            return;
+
+        args.Message = "airlock-component-cannot-pry-is-bolted-message";
+
+        args.Cancelled = true;
+    }
+
+    private void OnBeforeDoorOpened(EntityUid uid, DoorBoltComponent component, BeforeDoorOpenedEvent args)
+    {
+        if (component.BoltsDown)
+            args.Cancel();
+    }
+
+    private void OnBeforeDoorClosed(EntityUid uid, DoorBoltComponent component, BeforeDoorClosedEvent args)
+    {
+        if (component.BoltsDown)
+            args.Cancel();
+    }
+
+    private void OnBeforeDoorDenied(EntityUid uid, DoorBoltComponent component, BeforeDoorDeniedEvent args)
+    {
+        if (component.BoltsDown)
+            args.Cancel();
+    }
+
+    public void SetBoltWireCut(Entity<DoorBoltComponent> ent, bool value)
+    {
+        ent.Comp.BoltWireCut = value;
+        Dirty(ent, ent.Comp);
+    }
+
+    public void UpdateBoltLightStatus(Entity<DoorBoltComponent> ent)
+    {
+        AppearanceSystem.SetData(ent, DoorVisuals.BoltLights, GetBoltLightsVisible(ent));
+    }
+
+    public bool GetBoltLightsVisible(Entity<DoorBoltComponent> ent)
+    {
+        return ent.Comp.BoltLightsEnabled &&
+               ent.Comp.BoltsDown &&
+               ent.Comp.Powered;
+    }
+
+    public void SetBoltLightsEnabled(Entity<DoorBoltComponent> ent, bool value)
+    {
+        if (ent.Comp.BoltLightsEnabled == value)
+            return;
+
+        ent.Comp.BoltLightsEnabled = value;
+        Dirty(ent, ent.Comp);
+        UpdateBoltLightStatus(ent);
+    }
+
+    public void SetBoltsDown(Entity<DoorBoltComponent> ent, bool value, EntityUid? user = null, bool predicted = false)
+    {
+        if (ent.Comp.BoltsDown == value)
+            return;
+
+        ent.Comp.BoltsDown = value;
+        Dirty(ent, ent.Comp);
+        UpdateBoltLightStatus(ent);
+
+        var sound = value ? ent.Comp.BoltDownSound : ent.Comp.BoltUpSound;
+        if (predicted)
+            Audio.PlayPredicted(sound, ent, user: user);
+        else
+            Audio.PlayPvs(sound, ent);
+    }
+
+    private void OnStateChanged(Entity<DoorBoltComponent> entity, ref DoorStateChangedEvent args)
+    {
+        // If the door is closed, we should look if the bolt was locked while closing
+        UpdateBoltLightStatus(entity);
+    }
+
+    public bool IsBolted(EntityUid uid, DoorBoltComponent? component = null)
+    {
+        if (!Resolve(uid, ref component))
+        {
+            return false;
+        }
+
+        return component.BoltsDown;
+    }
+}
index f04f6c6dfe01311182d090c045b511f376644c72..6a2a25f614b927ad97a170392329efddcb559371 100644 (file)
@@ -1,26 +1,35 @@
 using System.Linq;
 using Content.Shared.Access.Components;
 using Content.Shared.Access.Systems;
+using Content.Shared.Administration.Logs;
 using Content.Shared.Damage;
+using Content.Shared.Database;
 using Content.Shared.Doors.Components;
+using Content.Shared.Emag.Systems;
 using Content.Shared.Hands.Components;
 using Content.Shared.Interaction;
 using Content.Shared.Physics;
+using Content.Shared.Popups;
 using Content.Shared.Prying.Components;
+using Content.Shared.Prying.Systems;
 using Content.Shared.Stunnable;
 using Content.Shared.Tag;
+using Content.Shared.Tools.Systems;
 using Robust.Shared.Audio;
 using Robust.Shared.Physics.Components;
 using Robust.Shared.Physics.Events;
 using Robust.Shared.Physics.Systems;
 using Robust.Shared.Timing;
 using Robust.Shared.Audio.Systems;
+using Robust.Shared.Network;
 
 namespace Content.Shared.Doors.Systems;
 
-public abstract class SharedDoorSystem : EntitySystem
+public abstract partial class SharedDoorSystem : EntitySystem
 {
+    [Dependency] private readonly ISharedAdminLogManager _adminLog = default!;
     [Dependency] protected readonly IGameTiming GameTiming = default!;
+    [Dependency] private readonly INetManager _net = default!;
     [Dependency] protected readonly SharedPhysicsSystem PhysicsSystem = default!;
     [Dependency] private readonly DamageableSystem _damageableSystem = default!;
     [Dependency] private readonly SharedStunSystem _stunSystem = default!;
@@ -30,6 +39,11 @@ public abstract class SharedDoorSystem : EntitySystem
     [Dependency] protected readonly SharedAppearanceSystem AppearanceSystem = default!;
     [Dependency] private readonly OccluderSystem _occluder = default!;
     [Dependency] private readonly AccessReaderSystem _accessReaderSystem = default!;
+    [Dependency] private readonly PryingSystem _pryingSystem = default!;
+    [Dependency] protected readonly SharedPopupSystem Popup = default!;
+
+    [ValidatePrototypeId<TagPrototype>]
+    public const string DoorBumpTag = "DoorBumpOpener";
 
     /// <summary>
     ///     A body must have an intersection percentage larger than this in order to be considered as colliding with a
@@ -50,6 +64,8 @@ public abstract class SharedDoorSystem : EntitySystem
     {
         base.Initialize();
 
+        InitializeBolts();
+
         SubscribeLocalEvent<DoorComponent, ComponentInit>(OnComponentInit);
         SubscribeLocalEvent<DoorComponent, ComponentRemove>(OnRemove);
 
@@ -60,8 +76,12 @@ public abstract class SharedDoorSystem : EntitySystem
         SubscribeLocalEvent<DoorComponent, StartCollideEvent>(HandleCollide);
         SubscribeLocalEvent<DoorComponent, PreventCollideEvent>(PreventCollision);
         SubscribeLocalEvent<DoorComponent, BeforePryEvent>(OnBeforePry);
+        SubscribeLocalEvent<DoorComponent, PriedEvent>(OnAfterPry);
+        SubscribeLocalEvent<DoorComponent, WeldableAttemptEvent>(OnWeldAttempt);
+        SubscribeLocalEvent<DoorComponent, WeldableChangedEvent>(OnWeldChanged);
         SubscribeLocalEvent<DoorComponent, GetPryTimeModifierEvent>(OnPryTimeModifier);
 
+        SubscribeLocalEvent<DoorComponent, GotEmaggedEvent>(OnEmagged);
     }
 
     protected virtual void OnComponentInit(Entity<DoorComponent> ent, ref ComponentInit args)
@@ -100,6 +120,23 @@ public abstract class SharedDoorSystem : EntitySystem
         _activeDoors.Remove(door);
     }
 
+    private void OnEmagged(EntityUid uid, DoorComponent door, ref GotEmaggedEvent args)
+    {
+        if (!TryComp<AirlockComponent>(uid, out var airlock))
+            return;
+
+        if (IsBolted(uid) || !airlock.Powered)
+            return;
+
+        if (door.State == DoorState.Closed)
+        {
+            if (!SetState(uid, DoorState.Emagging, door))
+                return;
+            Audio.PlayPredicted(door.SparkSound, uid, args.UserUid, AudioParams.Default.WithVolume(8));
+            args.Handled = true;
+        }
+    }
+
     #region StateManagement
     private void OnHandleState(Entity<DoorComponent> ent, ref AfterAutoHandleStateEvent args)
     {
@@ -113,14 +150,14 @@ public abstract class SharedDoorSystem : EntitySystem
         AppearanceSystem.SetData(ent, DoorVisuals.State, door.State);
     }
 
-    protected void SetState(EntityUid uid, DoorState state, DoorComponent? door = null)
+    protected bool SetState(EntityUid uid, DoorState state, DoorComponent? door = null)
     {
         if (!Resolve(uid, ref door))
-            return;
+            return false;
 
         // If no change, return to avoid firing a new DoorStateChangedEvent.
         if (state == door.State)
-            return;
+            return false;
 
         switch (state)
         {
@@ -159,14 +196,20 @@ public abstract class SharedDoorSystem : EntitySystem
         Dirty(uid, door);
         RaiseLocalEvent(uid, new DoorStateChangedEvent(state));
         AppearanceSystem.SetData(uid, DoorVisuals.State, door.State);
+        return true;
     }
 
     #endregion
 
     #region Interactions
-    protected virtual void OnActivate(EntityUid uid, DoorComponent door, ActivateInWorldEvent args)
+    protected void OnActivate(EntityUid uid, DoorComponent door, ActivateInWorldEvent args)
     {
-        // avoid client-mispredicts, as the server will definitely handle this event
+        if (args.Handled || !door.ClickOpen)
+            return;
+
+        if (!TryToggleDoor(uid, door, args.User, predicted: true))
+            _pryingSystem.TryPry(uid, args.User, out _);
+
         args.Handled = true;
     }
 
@@ -181,6 +224,44 @@ public abstract class SharedDoorSystem : EntitySystem
             args.Cancelled = true;
     }
 
+    /// <summary>
+    ///     Open or close a door after it has been successfully pried.
+    /// </summary>
+    private void OnAfterPry(EntityUid uid, DoorComponent door, ref PriedEvent args)
+    {
+        if (door.State == DoorState.Closed)
+        {
+            _adminLog.Add(LogType.Action, LogImpact.Medium, $"{ToPrettyString(args.User)} pried {ToPrettyString(uid)} open");
+            StartOpening(uid, door, args.User, true);
+        }
+        else if (door.State == DoorState.Open)
+        {
+            _adminLog.Add(LogType.Action, LogImpact.Medium, $"{ToPrettyString(args.User)} pried {ToPrettyString(uid)} closed");
+            StartClosing(uid, door, args.User, true);
+        }
+    }
+
+    private void OnWeldAttempt(EntityUid uid, DoorComponent component, WeldableAttemptEvent args)
+    {
+        if (component.CurrentlyCrushing.Count > 0)
+        {
+            args.Cancel();
+            return;
+        }
+        if (component.State != DoorState.Closed && component.State != DoorState.Welded)
+        {
+            args.Cancel();
+        }
+    }
+
+    private void OnWeldChanged(EntityUid uid, DoorComponent component, ref WeldableChangedEvent args)
+    {
+        if (component.State == DoorState.Closed)
+            SetState(uid, DoorState.Welded, component);
+        else if (component.State == DoorState.Welded)
+            SetState(uid, DoorState.Closed, component);
+    }
+
     /// <summary>
     ///     Update the door state/visuals and play an access denied sound when a user without access interacts with the
     ///     door.
@@ -199,13 +280,15 @@ public abstract class SharedDoorSystem : EntitySystem
         if (ev.Cancelled)
             return;
 
-        SetState(uid, DoorState.Denying, door);
+        if (!SetState(uid, DoorState.Denying, door))
+            return;
 
-        if (door.DenySound != null)
-            PlaySound(uid, door.DenySound, AudioParams.Default.WithVolume(-3), user, predicted);
+        if (predicted)
+            Audio.PlayPredicted(door.DenySound, uid, user, AudioParams.Default.WithVolume(-3));
+        else if (_net.IsServer)
+            Audio.PlayPvs(door.DenySound, uid, AudioParams.Default.WithVolume(-3));
     }
 
-
     public bool TryToggleDoor(EntityUid uid, DoorComponent? door = null, EntityUid? user = null, bool predicted = false)
     {
         if (!Resolve(uid, ref door))
@@ -215,7 +298,8 @@ public abstract class SharedDoorSystem : EntitySystem
         {
             return TryOpen(uid, door, user, predicted, quiet: door.State == DoorState.Denying);
         }
-        else if (door.State == DoorState.Open)
+
+        if (door.State == DoorState.Open)
         {
             return TryClose(uid, door, user, predicted);
         }
@@ -254,7 +338,7 @@ public abstract class SharedDoorSystem : EntitySystem
         if (!HasAccess(uid, user, door))
         {
             if (!quiet)
-                Deny(uid, door);
+                Deny(uid, door, user, predicted: true);
             return false;
         }
 
@@ -269,22 +353,29 @@ public abstract class SharedDoorSystem : EntitySystem
     /// <param name="user"> The user (if any) opening the door</param>
     /// <param name="predicted">Whether the interaction would have been
     /// predicted. See comments in the PlaySound method on the Server system for details</param>
-    public virtual void StartOpening(EntityUid uid, DoorComponent? door = null, EntityUid? user = null, bool predicted = false)
+    public void StartOpening(EntityUid uid, DoorComponent? door = null, EntityUid? user = null, bool predicted = false)
     {
         if (!Resolve(uid, ref door))
             return;
 
-        SetState(uid, DoorState.Opening, door);
+        var lastState = door.State;
 
-        if (door.OpenSound != null)
-            PlaySound(uid, door.OpenSound, AudioParams.Default.WithVolume(-5), user, predicted);
+        if (!SetState(uid, DoorState.Opening, door))
+            return;
+
+        if (predicted)
+            Audio.PlayPredicted(door.OpenSound, uid, user, AudioParams.Default.WithVolume(-5));
+        else if (_net.IsServer)
+            Audio.PlayPvs(door.OpenSound, uid, AudioParams.Default.WithVolume(-5));
+
+        if (lastState == DoorState.Emagging && TryComp<DoorBoltComponent>(uid, out var doorBoltComponent))
+            SetBoltsDown((uid, doorBoltComponent), !doorBoltComponent.BoltsDown, user, true);
 
         // I'm not sure what the intent here is/was? It plays a sound if the user is opening a door with a hands
         // component, but no actual hands!? What!? Is this the sound of them head-butting the door to get it to open??
         // I'm 99% sure something is wrong here, but I kind of want to keep it this way.
-
-        if (user != null && TryComp(user.Value, out HandsComponent? hands) && hands.Hands.Count == 0)
-            PlaySound(uid, door.TryOpenDoorSound, AudioParams.Default.WithVolume(-2), user, predicted);
+        if (user != null && (!TryComp(user.Value, out HandsComponent? hands) || hands.Hands.Count == 0))
+            Audio.PlayPredicted(door.TryOpenDoorSound, uid, user, AudioParams.Default.WithVolume(-2));
     }
 
     /// <summary>
@@ -310,7 +401,7 @@ public abstract class SharedDoorSystem : EntitySystem
         if (!Resolve(uid, ref door))
             return false;
 
-        if (!CanClose(uid, door, user, false))
+        if (!CanClose(uid, door, user))
             return false;
 
         StartClosing(uid, door, user, predicted);
@@ -323,9 +414,7 @@ public abstract class SharedDoorSystem : EntitySystem
     /// <param name="uid"> The uid of the door</param>
     /// <param name="door"> The doorcomponent of the door</param>
     /// <param name="user"> The user (if any) opening the door</param>
-    /// <param name="predicted">Whether the interaction would have been
-    /// predicted. See comments in the PlaySound method on the Server system for details</param>
-    public bool CanClose(EntityUid uid, DoorComponent? door = null, EntityUid? user = null, bool quiet = true)
+    public bool CanClose(EntityUid uid, DoorComponent? door = null, EntityUid? user = null)
     {
         if (!Resolve(uid, ref door))
             return false;
@@ -336,7 +425,7 @@ public abstract class SharedDoorSystem : EntitySystem
             return false;
 
         var ev = new BeforeDoorClosedEvent(door.PerformCollisionCheck);
-        RaiseLocalEvent(uid, ev, false);
+        RaiseLocalEvent(uid, ev);
         if (ev.Cancelled)
             return false;
 
@@ -346,15 +435,18 @@ public abstract class SharedDoorSystem : EntitySystem
         return !ev.PerformCollisionCheck || !GetColliding(uid).Any();
     }
 
-    public virtual void StartClosing(EntityUid uid, DoorComponent? door = null, EntityUid? user = null, bool predicted = false)
+    public void StartClosing(EntityUid uid, DoorComponent? door = null, EntityUid? user = null, bool predicted = false)
     {
         if (!Resolve(uid, ref door))
             return;
 
-        SetState(uid, DoorState.Closing, door);
+        if (!SetState(uid, DoorState.Closing, door))
+            return;
 
-        if (door.CloseSound != null)
-            PlaySound(uid, door.CloseSound, AudioParams.Default.WithVolume(-5), user, predicted);
+        if (predicted)
+            Audio.PlayPredicted(door.CloseSound, uid, user, AudioParams.Default.WithVolume(-5));
+        else if (_net.IsServer)
+            Audio.PlayPvs(door.CloseSound, uid, AudioParams.Default.WithVolume(-5));
     }
 
     /// <summary>
@@ -367,7 +459,6 @@ public abstract class SharedDoorSystem : EntitySystem
             return false;
 
         door.Partial = true;
-        Dirty(uid, door);
 
         // Make sure no entity waled into the airlock when it started closing.
         if (!CanClose(uid, door))
@@ -380,6 +471,7 @@ public abstract class SharedDoorSystem : EntitySystem
 
         SetCollidable(uid, true, door, physics);
         door.NextStateChange = GameTiming.CurTime + door.CloseTimeTwo;
+        Dirty(uid, door);
         _activeDoors.Add((uid, door));
 
         // Crush any entities. Note that we don't check airlock safety here. This should have been checked before
@@ -489,10 +581,22 @@ public abstract class SharedDoorSystem : EntitySystem
         }
     }
 
-    protected virtual void HandleCollide(EntityUid uid, DoorComponent door, ref StartCollideEvent args)
+    /// <summary>
+    ///     Open a door if a player or door-bumper (PDA, ID-card) collide with the door. Sadly, bullets no longer
+    ///     generate "access denied" sounds as you fire at a door.
+    /// </summary>
+    private void HandleCollide(EntityUid uid, DoorComponent door, ref StartCollideEvent args)
     {
-        // TODO ACCESS READER move access reader to shared and predict door opening/closing
-        // Then this can be moved to the shared system without mispredicting.
+        if (!door.BumpOpen)
+            return;
+
+        if (door.State is not (DoorState.Closed or DoorState.Denying))
+            return;
+
+        var otherUid = args.OtherEntity;
+
+        if (Tags.HasTag(otherUid, DoorBumpTag))
+            TryOpen(uid, door, otherUid, quiet: door.State == DoorState.Denying);
     }
     #endregion
 
@@ -587,6 +691,19 @@ public abstract class SharedDoorSystem : EntitySystem
         _activeDoors.Add((uid, door));
     }
 
+    protected void CheckDoorBump(Entity<DoorComponent, PhysicsComponent> ent)
+    {
+        var (uid, door, physics) = ent;
+        if (door.BumpOpen)
+        {
+            foreach (var other in PhysicsSystem.GetContactingEntities(uid, physics, approximate: true))
+            {
+                if (Tags.HasTag(other, DoorBumpTag) && TryOpen(uid, door, other, quiet: true))
+                    break;
+            }
+        }
+    }
+
     /// <summary>
     ///     Iterate over active doors and progress them to the next state if they need to be updated.
     /// </summary>
@@ -619,8 +736,6 @@ public abstract class SharedDoorSystem : EntitySystem
         }
     }
 
-    protected virtual void CheckDoorBump(Entity<DoorComponent, PhysicsComponent> ent) { }
-
     /// <summary>
     ///     Makes a door proceed to the next state (if applicable).
     /// </summary>
@@ -632,7 +747,7 @@ public abstract class SharedDoorSystem : EntitySystem
         if (door.CurrentlyCrushing.Count > 0)
             // This is a closed door that is crushing people and needs to auto-open. Note that we don't check "can open"
             // here. The door never actually finished closing and we don't want people to get stuck inside of doors.
-            StartOpening(ent, door, predicted: true);
+            StartOpening(ent, door);
 
         switch (door.State)
         {
@@ -665,7 +780,7 @@ public abstract class SharedDoorSystem : EntitySystem
 
             case DoorState.Open:
                 // This door is open, and queued for an auto-close.
-                if (!TryClose(ent, door, predicted: true))
+                if (!TryClose(ent, door))
                 {
                     // The door failed to close (blocked?). Try again in one second.
                     door.NextStateChange = time + TimeSpan.FromSeconds(1);
@@ -679,6 +794,4 @@ public abstract class SharedDoorSystem : EntitySystem
         }
     }
     #endregion
-
-    protected abstract void PlaySound(EntityUid uid, SoundSpecifier soundSpecifier, AudioParams audioParams, EntityUid? predictingPlayer, bool predicted);
 }
index 7271258f5c4acbfbbb555c860754081ba27e5f60..fa7a135e6c525cca5a6ffbf07be9f542f538c8d8 100644 (file)
@@ -74,8 +74,8 @@ public sealed class PryingSystem : EntitySystem
 
         if (!CanPry(target, user, out var message, comp))
         {
-            if (message != null)
-                Popup.PopupEntity(Loc.GetString(message), target, user);
+            if (!string.IsNullOrWhiteSpace(message))
+                Popup.PopupClient(Loc.GetString(message), target, user);
             // If we have reached this point we want the event that caused this
             // to be marked as handled.
             return true;
@@ -162,23 +162,14 @@ public sealed class PryingSystem : EntitySystem
 
         if (!CanPry(uid, args.User, out var message, comp))
         {
-            if (message != null)
-                Popup.PopupEntity(Loc.GetString(message), uid, args.User);
+            if (!string.IsNullOrWhiteSpace(message))
+                Popup.PopupClient(Loc.GetString(message), uid, args.User);
             return;
         }
 
-        // TODO: When we get airlock prediction make this fully predicted.
-        // When that happens also fix the checking function in the Client AirlockSystem.
         if (args.Used != null && comp != null)
         {
-            if (HasComp<AirlockComponent>(uid))
-            {
-                _audioSystem.PlayPvs(comp.UseSound, args.Used.Value);
-            }
-            else
-            {
-                _audioSystem.PlayPredicted(comp.UseSound, args.Used.Value, args.User);
-            }
+            _audioSystem.PlayPredicted(comp.UseSound, args.Used.Value, args.User);
         }
 
         var ev = new PriedEvent(args.User);
index a40f42832991d2ed18623c4329039387f1e6440e..f069687ffb7da12194fc38f7e20894df5dbdf8fe 100644 (file)
@@ -76,6 +76,13 @@ public abstract class SharedWiresSystem : EntitySystem
         }
     }
 
+    public void ChangePanelVisibility(EntityUid uid, WiresPanelComponent component, bool visible)
+    {
+        component.Visible = visible;
+        UpdateAppearance(uid, component);
+        Dirty(uid, component);
+    }
+
     protected void UpdateAppearance(EntityUid uid, WiresPanelComponent panel)
     {
         if (TryComp<AppearanceComponent>(uid, out var appearance))