diff --git a/Content.Client/Doors/AirlockSystem.cs b/Content.Client/Doors/AirlockSystem.cs index 4837adf460..58c8730643 100644 --- a/Content.Client/Doors/AirlockSystem.cs +++ b/Content.Client/Doors/AirlockSystem.cs @@ -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(OnComponentStartup); SubscribeLocalEvent(OnAppearanceChange); - SubscribeLocalEvent(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(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 index 58144cd6e0..0000000000 --- a/Content.Client/Doors/DoorBoltSystem.cs +++ /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. -} diff --git a/Content.Client/Doors/DoorSystem.cs b/Content.Client/Doors/DoorSystem.cs index 80ce47e0c2..473ae97059 100644 --- a/Content.Client/Doors/DoorSystem.cs +++ b/Content.Client/Doors/DoorSystem.cs @@ -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(uid, DoorVisuals.State, out var state, args.Component)) @@ -83,9 +81,9 @@ public sealed class DoorSystem : SharedDoorSystem { if (!_resourceCache.TryGetResource(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); - } } diff --git a/Content.Server/Administration/Systems/AdminVerbSystem.Tools.cs b/Content.Server/Administration/Systems/AdminVerbSystem.Tools.cs index 296e48274c..c68336deab 100644 --- a/Content.Server/Administration/Systems/AdminVerbSystem.Tools.cs +++ b/Content.Server/Administration/Systems/AdminVerbSystem.Tools.cs @@ -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 diff --git a/Content.Server/DeviceLinking/Systems/DoorSignalControlSystem.cs b/Content.Server/DeviceLinking/Systems/DoorSignalControlSystem.cs index 1c0c9713cf..fab9a306ae 100644 --- a/Content.Server/DeviceLinking/Systems/DoorSignalControlSystem.cs +++ b/Content.Server/DeviceLinking/Systems/DoorSignalControlSystem.cs @@ -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); } } diff --git a/Content.Server/Doors/Systems/AirlockSystem.cs b/Content.Server/Doors/Systems/AirlockSystem.cs index 6d9cde8eb4..71f9347e9e 100644 --- a/Content.Server/Doors/Systems/AirlockSystem.cs +++ b/Content.Server/Doors/Systems/AirlockSystem.cs @@ -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(OnSignalReceived); SubscribeLocalEvent(OnPowerChanged); - SubscribeLocalEvent(OnStateChanged); - SubscribeLocalEvent(OnBeforeDoorOpened); - SubscribeLocalEvent(OnBeforeDoorDenied); SubscribeLocalEvent(OnActivate, before: new[] { typeof(DoorSystem) }); - SubscribeLocalEvent(OnGetPryMod); - SubscribeLocalEvent(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(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(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; - } - - /// - /// Updates the auto close timer. - /// - 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(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 index 133af0013d..0000000000 --- a/Content.Server/Doors/Systems/DoorBoltSystem.cs +++ /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(OnPowerChanged); - SubscribeLocalEvent(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(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; - } -} - diff --git a/Content.Server/Doors/Systems/DoorSystem.cs b/Content.Server/Doors/Systems/DoorSystem.cs index d59ea59622..5968e445c1 100644 --- a/Content.Server/Doors/Systems/DoorSystem.cs +++ b/Content.Server/Doors/Systems/DoorSystem.cs @@ -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(OnWeldAttempt); - SubscribeLocalEvent(OnWeldChanged); - SubscribeLocalEvent(OnEmagged); - SubscribeLocalEvent(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(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. - - /// - /// Selectively send sound to clients, taking care to not send the double-audio. - /// - /// The audio source - /// The sound - /// The audio parameters. - /// The user (if any) that instigated an interaction - /// 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. - protected override void PlaySound(EntityUid uid, SoundSpecifier soundSpecifier, AudioParams audioParams, EntityUid? predictingPlayer, bool predicted) + private void OnBoltPowerChanged(Entity ent, ref PowerChangedEvent args) { - // 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) + if (args.Powered) { - args.Cancel(); - return; + if (ent.Comp.BoltWireCut) + SetBoltsDown(ent, true); } - 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 - - - /// - /// 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. - /// - 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(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(uid, out var doorBoltComponent)) - _bolts.SetBoltsWithAudio(uid, doorBoltComponent, !doorBoltComponent.BoltsDown); - } - - /// - /// Open or close a door after it has been successfuly pried. - /// - 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); - } - 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 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); } } diff --git a/Content.Server/Doors/WireActions/DoorBoltLightWireAction.cs b/Content.Server/Doors/WireActions/DoorBoltLightWireAction.cs index 233ebfed3f..c5e3aa80c9 100644 --- a/Content.Server/Doors/WireActions/DoorBoltLightWireAction.cs +++ b/Content.Server/Doors/WireActions/DoorBoltLightWireAction.cs @@ -18,19 +18,18 @@ public sealed partial class DoorBoltLightWireAction : ComponentWireAction().SetBoltLightsEnabled(wire.Owner, door, false); + EntityManager.System().SetBoltLightsEnabled((wire.Owner, door), false); return true; } public override bool Mend(EntityUid user, Wire wire, DoorBoltComponent door) { - - EntityManager.System().SetBoltLightsEnabled(wire.Owner, door, true); + EntityManager.System().SetBoltLightsEnabled((wire.Owner, door), true); return true; } public override void Pulse(EntityUid user, Wire wire, DoorBoltComponent door) { - EntityManager.System().SetBoltLightsEnabled(wire.Owner, door, !door.BoltLightsEnabled); + EntityManager.System().SetBoltLightsEnabled((wire.Owner, door), !door.BoltLightsEnabled); } } diff --git a/Content.Server/Doors/WireActions/DoorBoltWireAction.cs b/Content.Server/Doors/WireActions/DoorBoltWireAction.cs index 29b5d726a2..fc1cf50cd8 100644 --- a/Content.Server/Doors/WireActions/DoorBoltWireAction.cs +++ b/Content.Server/Doors/WireActions/DoorBoltWireAction.cs @@ -19,24 +19,24 @@ public sealed partial class DoorBoltWireAction : ComponentWireAction().SetBoltWireCut(airlock, true); + EntityManager.System().SetBoltWireCut((wire.Owner, airlock), true); if (!airlock.BoltsDown && IsPowered(wire.Owner)) - EntityManager.System().SetBoltsWithAudio(wire.Owner, airlock, true); + EntityManager.System().SetBoltsDown((wire.Owner, airlock), true, user); return true; } public override bool Mend(EntityUid user, Wire wire, DoorBoltComponent door) { - EntityManager.System().SetBoltWireCut(door, false); + EntityManager.System().SetBoltWireCut((wire.Owner, door), false); return true; } public override void Pulse(EntityUid user, Wire wire, DoorBoltComponent door) { if (IsPowered(wire.Owner)) - EntityManager.System().SetBoltsWithAudio(wire.Owner, door, !door.BoltsDown); + EntityManager.System().SetBoltsDown((wire.Owner, door), !door.BoltsDown); else if (!door.BoltsDown) - EntityManager.System().SetBoltsWithAudio(wire.Owner, door, true); + EntityManager.System().SetBoltsDown((wire.Owner, door), true); } } diff --git a/Content.Server/Magic/MagicSystem.cs b/Content.Server/Magic/MagicSystem.cs index d8febe0d1f..2c9b11dbd3 100644 --- a/Content.Server/Magic/MagicSystem.cs +++ b/Content.Server/Magic/MagicSystem.cs @@ -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(entity, out var bolts)) - _boltsSystem.SetBoltsDown(entity, bolts, false); + _doorSystem.SetBoltsDown((entity, bolts), false); if (TryComp(entity, out var doorComp) && doorComp.State is not DoorState.Open) _doorSystem.StartOpening(entity); diff --git a/Content.Server/Remotes/DoorRemoteSystem.cs b/Content.Server/Remotes/DoorRemoteSystem.cs index a88508ce53..9be7e5e96b 100644 --- a/Content.Server/Remotes/DoorRemoteSystem.cs +++ b/Content.Server/Remotes/DoorRemoteSystem.cs @@ -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"); } } diff --git a/Content.Server/Shuttles/Systems/DockingSystem.cs b/Content.Server/Shuttles/Systems/DockingSystem.cs index 778d244376..7f69885045 100644 --- a/Content.Server/Shuttles/Systems/DockingSystem.cs +++ b/Content.Server/Shuttles/Systems/DockingSystem.cs @@ -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(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(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(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; diff --git a/Content.Server/Shuttles/Systems/ShuttleSystem.FasterThanLight.cs b/Content.Server/Shuttles/Systems/ShuttleSystem.FasterThanLight.cs index 99ce4a044d..d976b634d5 100644 --- a/Content.Server/Shuttles/Systems/ShuttleSystem.FasterThanLight.cs +++ b/Content.Server/Shuttles/Systems/ShuttleSystem.FasterThanLight.cs @@ -459,7 +459,7 @@ public sealed partial class ShuttleSystem continue; _doors.TryClose(doorUid); - _bolts.SetBoltsWithAudio(doorUid, door, enabled); + _doors.SetBoltsDown((doorUid, door), enabled); } } diff --git a/Content.Server/Shuttles/Systems/ShuttleSystem.cs b/Content.Server/Shuttles/Systems/ShuttleSystem.cs index 471b46e507..5f11ce25a4 100644 --- a/Content.Server/Shuttles/Systems/ShuttleSystem.cs +++ b/Content.Server/Shuttles/Systems/ShuttleSystem.cs @@ -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!; diff --git a/Content.Server/Wires/WiresSystem.cs b/Content.Server/Wires/WiresSystem.cs index 2b23b09360..0f9ee8b0f7 100644 --- a/Content.Server/Wires/WiresSystem.cs +++ b/Content.Server/Wires/WiresSystem.cs @@ -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; diff --git a/Content.Shared/Doors/Components/AirlockComponent.cs b/Content.Shared/Doors/Components/AirlockComponent.cs index 332df2ac77..dd8241c64e 100644 --- a/Content.Shared/Doors/Components/AirlockComponent.cs +++ b/Content.Shared/Doors/Components/AirlockComponent.cs @@ -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] diff --git a/Content.Shared/Doors/Components/DoorBoltComponent.cs b/Content.Shared/Doors/Components/DoorBoltComponent.cs index 52f7d56db5..a5b0621ff4 100644 --- a/Content.Shared/Doors/Components/DoorBoltComponent.cs +++ b/Content.Shared/Doors/Components/DoorBoltComponent.cs @@ -8,36 +8,43 @@ namespace Content.Shared.Doors.Components; /// Companion component to DoorComponent that handles bolt-specific behavior. /// [RegisterComponent, NetworkedComponent] -[Access(typeof(SharedDoorBoltSystem))] +[Access(typeof(SharedDoorSystem))] +[AutoGenerateComponentState] public sealed partial class DoorBoltComponent : Component { /// /// Sound to play when the bolts on the airlock go up. /// - [DataField("boltUpSound"), ViewVariables(VVAccess.ReadWrite)] + [DataField, ViewVariables(VVAccess.ReadWrite)] public SoundSpecifier BoltUpSound = new SoundPathSpecifier("/Audio/Machines/boltsup.ogg"); /// /// Sound to play when the bolts on the airlock go down. /// - [DataField("boltDownSound"), ViewVariables(VVAccess.ReadWrite)] + [DataField, ViewVariables(VVAccess.ReadWrite)] public SoundSpecifier BoltDownSound = new SoundPathSpecifier("/Audio/Machines/boltsdown.ogg"); /// /// Whether the door bolts are currently deployed. /// - [ViewVariables(VVAccess.ReadWrite)] + [DataField, AutoNetworkedField] public bool BoltsDown; /// /// Whether the bolt lights are currently enabled. /// - [ViewVariables(VVAccess.ReadWrite)] + [DataField, AutoNetworkedField] public bool BoltLightsEnabled = true; /// /// True if the bolt wire is cut, which will force the airlock to always be bolted as long as it has power. /// - [ViewVariables(VVAccess.ReadWrite)] + [DataField, AutoNetworkedField] public bool BoltWireCut; + + /// + /// Used for prediction. true if the door has power. + /// + [DataField, AutoNetworkedField] + public bool Powered; } diff --git a/Content.Shared/Doors/Components/DoorComponent.cs b/Content.Shared/Doors/Components/DoorComponent.cs index 135f8b0856..d1b93d845d 100644 --- a/Content.Shared/Doors/Components/DoorComponent.cs +++ b/Content.Shared/Doors/Components/DoorComponent.cs @@ -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. /// - [DataField] + [DataField, AutoNetworkedField] public bool CanCrush = true; /// /// Whether to check for colliding entities before closing. This may be overridden by other system by subscribing to /// . For example, hacked airlocks will set this to false. /// - [DataField] + [DataField, AutoNetworkedField] public bool PerformCollisionCheck = true; /// @@ -190,7 +190,7 @@ public sealed partial class DoorComponent : Component /// The sprite state used for the door when it's being emagged. /// [DataField] - public string EmaggingSpriteState = "emagging"; + public string EmaggingSpriteState = "sparks"; /// /// The sprite state used for the door when it's open. diff --git a/Content.Shared/Doors/Systems/SharedAirlockSystem.cs b/Content.Shared/Doors/Systems/SharedAirlockSystem.cs index 9e1273b784..a3172bb8c3 100644 --- a/Content.Shared/Doors/Systems/SharedAirlockSystem.cs +++ b/Content.Shared/Doors/Systems/SharedAirlockSystem.cs @@ -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(OnBeforeDoorClosed); + SubscribeLocalEvent(OnStateChanged); + SubscribeLocalEvent(OnBeforeDoorOpened); + SubscribeLocalEvent(OnBeforeDoorDenied); + SubscribeLocalEvent(OnGetPryMod); + SubscribeLocalEvent(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(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; + } + + /// + /// Updates the auto close timer. + /// + 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 index 7c5ef45275..0000000000 --- a/Content.Shared/Doors/Systems/SharedDoorBoltSystem.cs +++ /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(OnBeforeDoorOpened); - SubscribeLocalEvent(OnBeforeDoorClosed); - SubscribeLocalEvent(OnBeforeDoorDenied); - SubscribeLocalEvent(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 index 0000000000..35681bfd82 --- /dev/null +++ b/Content.Shared/Doors/Systems/SharedDoorSystem.Bolts.cs @@ -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(OnBeforeDoorOpened); + SubscribeLocalEvent(OnBeforeDoorClosed); + SubscribeLocalEvent(OnBeforeDoorDenied); + SubscribeLocalEvent(OnDoorPry); + SubscribeLocalEvent(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 ent, bool value) + { + ent.Comp.BoltWireCut = value; + Dirty(ent, ent.Comp); + } + + public void UpdateBoltLightStatus(Entity ent) + { + AppearanceSystem.SetData(ent, DoorVisuals.BoltLights, GetBoltLightsVisible(ent)); + } + + public bool GetBoltLightsVisible(Entity ent) + { + return ent.Comp.BoltLightsEnabled && + ent.Comp.BoltsDown && + ent.Comp.Powered; + } + + public void SetBoltLightsEnabled(Entity ent, bool value) + { + if (ent.Comp.BoltLightsEnabled == value) + return; + + ent.Comp.BoltLightsEnabled = value; + Dirty(ent, ent.Comp); + UpdateBoltLightStatus(ent); + } + + public void SetBoltsDown(Entity 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 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; + } +} diff --git a/Content.Shared/Doors/Systems/SharedDoorSystem.cs b/Content.Shared/Doors/Systems/SharedDoorSystem.cs index f04f6c6dfe..6a2a25f614 100644 --- a/Content.Shared/Doors/Systems/SharedDoorSystem.cs +++ b/Content.Shared/Doors/Systems/SharedDoorSystem.cs @@ -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] + public const string DoorBumpTag = "DoorBumpOpener"; /// /// 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(OnComponentInit); SubscribeLocalEvent(OnRemove); @@ -60,8 +76,12 @@ public abstract class SharedDoorSystem : EntitySystem SubscribeLocalEvent(HandleCollide); SubscribeLocalEvent(PreventCollision); SubscribeLocalEvent(OnBeforePry); + SubscribeLocalEvent(OnAfterPry); + SubscribeLocalEvent(OnWeldAttempt); + SubscribeLocalEvent(OnWeldChanged); SubscribeLocalEvent(OnPryTimeModifier); + SubscribeLocalEvent(OnEmagged); } protected virtual void OnComponentInit(Entity 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(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 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; } + /// + /// Open or close a door after it has been successfully pried. + /// + 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); + } + /// /// 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 /// The user (if any) opening the door /// Whether the interaction would have been /// predicted. See comments in the PlaySound method on the Server system for details - 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(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)); } /// @@ -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 /// The uid of the door /// The doorcomponent of the door /// The user (if any) opening the door - /// Whether the interaction would have been - /// predicted. See comments in the PlaySound method on the Server system for details - 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)); } /// @@ -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) + /// + /// 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. + /// + 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 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; + } + } + } + /// /// Iterate over active doors and progress them to the next state if they need to be updated. /// @@ -619,8 +736,6 @@ public abstract class SharedDoorSystem : EntitySystem } } - protected virtual void CheckDoorBump(Entity ent) { } - /// /// Makes a door proceed to the next state (if applicable). /// @@ -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); } diff --git a/Content.Shared/Prying/Systems/PryingSystem.cs b/Content.Shared/Prying/Systems/PryingSystem.cs index 7271258f5c..fa7a135e6c 100644 --- a/Content.Shared/Prying/Systems/PryingSystem.cs +++ b/Content.Shared/Prying/Systems/PryingSystem.cs @@ -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(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); diff --git a/Content.Shared/Wires/SharedWiresSystem.cs b/Content.Shared/Wires/SharedWiresSystem.cs index a40f428329..f069687ffb 100644 --- a/Content.Shared/Wires/SharedWiresSystem.cs +++ b/Content.Shared/Wires/SharedWiresSystem.cs @@ -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(uid, out var appearance))