diff --git a/Content.Client/Doors/AirlockVisualizer.cs b/Content.Client/Doors/AirlockVisualizer.cs index e2ff79362a..59aa77b97a 100644 --- a/Content.Client/Doors/AirlockVisualizer.cs +++ b/Content.Client/Doors/AirlockVisualizer.cs @@ -160,9 +160,6 @@ namespace Content.Client.Doors } var door = _entMan.GetComponent(component.Owner); - var unlitVisible = true; - var boltedVisible = false; - var emergencyLightsVisible = false; if (component.TryGetData(DoorVisuals.BaseRSI, out string baseRsi)) { @@ -184,7 +181,6 @@ namespace Content.Client.Doors { case DoorState.Open: sprite.LayerSetState(DoorVisualLayers.Base, "open"); - unlitVisible = _openUnlitVisible; if (_openUnlitVisible && !_simpleVisuals) { sprite.LayerSetState(DoorVisualLayers.BaseUnlit, "open_unlit"); @@ -219,33 +215,33 @@ namespace Content.Client.Doors throw new ArgumentOutOfRangeException(); } - if (component.TryGetData(DoorVisuals.Powered, out bool powered) && !powered) + if (_simpleVisuals) + return; + + var boltedVisible = false; + var emergencyLightsVisible = false; + var unlitVisible = false; + + if (component.TryGetData(DoorVisuals.Powered, out bool powered) && powered) { - unlitVisible = false; - } - if (component.TryGetData(DoorVisuals.BoltLights, out bool lights) && lights) - { - boltedVisible = true; + boltedVisible = component.TryGetData(DoorVisuals.BoltLights, out bool lights) && lights; + emergencyLightsVisible = component.TryGetData(DoorVisuals.EmergencyLights, out bool eaLights) && eaLights; + unlitVisible = state == DoorState.Closing + || state == DoorState.Opening + || state == DoorState.Denying + || state == DoorState.Open && _openUnlitVisible + || (component.TryGetData(DoorVisuals.ClosedLights, out bool closedLights) && closedLights); } - if (component.TryGetData(DoorVisuals.EmergencyLights, out bool eaLights) && eaLights) + sprite.LayerSetVisible(DoorVisualLayers.BaseUnlit, unlitVisible); + sprite.LayerSetVisible(DoorVisualLayers.BaseBolted, boltedVisible); + if (_emergencyAccessLayer) { - emergencyLightsVisible = true; - } - - if (!_simpleVisuals) - { - sprite.LayerSetVisible(DoorVisualLayers.BaseUnlit, unlitVisible && state != DoorState.Closed && state != DoorState.Welded); - sprite.LayerSetVisible(DoorVisualLayers.BaseBolted, unlitVisible && boltedVisible); - if (_emergencyAccessLayer) - { - sprite.LayerSetVisible(DoorVisualLayers.BaseEmergencyAccess, - emergencyLightsVisible - && state != DoorState.Open - && state != DoorState.Opening - && state != DoorState.Closing - && unlitVisible); - } + sprite.LayerSetVisible(DoorVisualLayers.BaseEmergencyAccess, + emergencyLightsVisible + && state != DoorState.Open + && state != DoorState.Opening + && state != DoorState.Closing); } } } diff --git a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.Monstermos.cs b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.Monstermos.cs index b31485654b..7be214e184 100644 --- a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.Monstermos.cs +++ b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.Monstermos.cs @@ -1,5 +1,6 @@ using Content.Server.Atmos.Components; using Content.Server.Doors.Components; +using Content.Server.Doors.Systems; using Content.Shared.Atmos; using Content.Shared.Database; using Robust.Shared.Map; @@ -10,6 +11,8 @@ namespace Content.Server.Atmos.EntitySystems { public sealed partial class AtmosphereSystem { + [Dependency] private readonly FirelockSystem _firelockSystem = default!; + private readonly TileAtmosphereComparer _monstermosComparer = new(); private readonly TileAtmosphere?[] _equalizeTiles = new TileAtmosphere[Atmospherics.MonstermosHardTileLimit]; @@ -502,7 +505,7 @@ namespace Content.Server.Atmos.EntitySystems if (!TryComp(entity, out FirelockComponent? firelock)) continue; - reconsiderAdjacent |= firelock.EmergencyPressureStop(); + reconsiderAdjacent |= _firelockSystem.EmergencyPressureStop(entity, firelock); } foreach (var entity in mapGrid.GetAnchoredEntities(other.GridIndices)) @@ -510,7 +513,7 @@ namespace Content.Server.Atmos.EntitySystems if (!TryComp(entity, out FirelockComponent? firelock)) continue; - reconsiderAdjacent |= firelock.EmergencyPressureStop(); + reconsiderAdjacent |= _firelockSystem.EmergencyPressureStop(entity, firelock); } if (!reconsiderAdjacent) diff --git a/Content.Server/Doors/Components/FirelockComponent.cs b/Content.Server/Doors/Components/FirelockComponent.cs index 248d080948..7bb35ad2c9 100644 --- a/Content.Server/Doors/Components/FirelockComponent.cs +++ b/Content.Server/Doors/Components/FirelockComponent.cs @@ -1,103 +1,35 @@ -using Content.Server.Atmos.Components; -using Content.Server.Atmos.EntitySystems; -using Content.Server.Doors.Systems; using Content.Shared.Doors.Components; -using Robust.Server.GameObjects; -using Robust.Shared.GameObjects; -using Robust.Shared.IoC; -using Robust.Shared.Serialization.Manager.Attributes; namespace Content.Server.Doors.Components { /// - /// Companion component to ServerDoorComponent that handles firelock-specific behavior -- primarily prying, - /// and not being openable on open-hand click. + /// Companion component to that handles firelock-specific behavior, including + /// auto-closing on depressurization, air/fire alarm interactions, and preventing normal door functions when + /// retaining pressure.. /// [RegisterComponent] public sealed class FirelockComponent : Component { - [Dependency] private readonly IEntityManager _entMan = default!; - /// /// Pry time modifier to be used when the firelock is currently closed due to fire or pressure. /// /// - [DataField("lockedPryTimeModifier")] + [DataField("lockedPryTimeModifier"), ViewVariables(VVAccess.ReadWrite)] public float LockedPryTimeModifier = 1.5f; [DataField("autocloseDelay")] public TimeSpan AutocloseDelay = TimeSpan.FromSeconds(3f); - public bool EmergencyPressureStop() - { - var doorSys = EntitySystem.Get(); - if (_entMan.TryGetComponent(Owner, out var door) && - door.State == DoorState.Open && - doorSys.CanClose(Owner, door)) - { - doorSys.StartClosing(Owner, door); + /// + /// Maximum pressure difference before the firelock will refuse to open, in kPa. + /// + [DataField("pressureThreshold"), ViewVariables(VVAccess.ReadWrite)] + public float PressureThreshold = 20; - // Door system also sets airtight, but only after a delay. We want it to be immediate. - if (_entMan.TryGetComponent(Owner, out AirtightComponent? airtight)) - { - EntitySystem.Get().SetAirblocked(airtight, true); - } - return true; - } - return false; - } - - public bool IsHoldingPressure(float threshold = 20) - { - var transform = _entMan.GetComponent(Owner); - - if (transform.GridUid is not {} gridUid) - return false; - - var atmosphereSystem = _entMan.EntitySysManager.GetEntitySystem(); - var transformSystem = _entMan.EntitySysManager.GetEntitySystem(); - - var position = transformSystem.GetGridOrMapTilePosition(Owner, transform); - - var minMoles = float.MaxValue; - var maxMoles = 0f; - - foreach (var adjacent in atmosphereSystem.GetAdjacentTileMixtures(gridUid, position)) - { - var moles = adjacent.TotalMoles; - if (moles < minMoles) - minMoles = moles; - if (moles > maxMoles) - maxMoles = moles; - } - - return (maxMoles - minMoles) > threshold; - } - - public bool IsHoldingFire() - { - var atmosphereSystem = _entMan.EntitySysManager.GetEntitySystem(); - var transformSystem = _entMan.EntitySysManager.GetEntitySystem(); - - var transform = _entMan.GetComponent(Owner); - var position = transformSystem.GetGridOrMapTilePosition(Owner, transform); - - // No grid, no fun. - if (transform.GridUid is not {} gridUid) - return false; - - if (atmosphereSystem.GetTileMixture(gridUid, null, position) == null) - return false; - - if (atmosphereSystem.IsHotspotActive(gridUid, position)) - return true; - - foreach (var adjacent in atmosphereSystem.GetAdjacentTiles(gridUid, position)) - { - if (atmosphereSystem.IsHotspotActive(gridUid, adjacent)) - return true; - } - - return false; - } + /// + /// If true, and if this door has an , then it will only auto-close if the + /// alarm is set to danger. + /// + [DataField("alarmAutoClose"), ViewVariables(VVAccess.ReadWrite)] + public bool AlarmAutoClose = true; } } diff --git a/Content.Server/Doors/Systems/FirelockSystem.cs b/Content.Server/Doors/Systems/FirelockSystem.cs index c066a06c61..cdc6be575f 100644 --- a/Content.Server/Doors/Systems/FirelockSystem.cs +++ b/Content.Server/Doors/Systems/FirelockSystem.cs @@ -1,68 +1,133 @@ -using Content.Server.Atmos.Monitor.Components; +using Content.Server.Atmos.Components; +using Content.Server.Atmos.EntitySystems; using Content.Server.Atmos.Monitor.Systems; using Content.Server.Doors.Components; +using Content.Server.Popups; using Content.Server.Power.EntitySystems; +using Content.Server.Power.Components; using Content.Shared.Atmos.Monitor; using Content.Shared.Doors; using Content.Shared.Doors.Components; using Content.Shared.Doors.Systems; -using Content.Shared.Popups; +using Robust.Server.GameObjects; +using Robust.Shared.Player; namespace Content.Server.Doors.Systems { public sealed class FirelockSystem : EntitySystem { + [Dependency] private readonly PopupSystem _popupSystem = default!; [Dependency] private readonly SharedDoorSystem _doorSystem = default!; [Dependency] private readonly AtmosAlarmableSystem _atmosAlarmable = default!; + [Dependency] private readonly AtmosphereSystem _atmosSystem = default!; + [Dependency] private readonly AirtightSystem _airtightSystem = default!; + + private static float _visualUpdateInterval = 0.2f; + private float _accumulatedFrameTime; public override void Initialize() { base.Initialize(); SubscribeLocalEvent(OnBeforeDoorOpened); - SubscribeLocalEvent(OnBeforeDoorDenied); SubscribeLocalEvent(OnDoorGetPryTimeModifier); - SubscribeLocalEvent(OnBeforeDoorPry); SubscribeLocalEvent(OnUpdateState); SubscribeLocalEvent(OnBeforeDoorAutoclose); SubscribeLocalEvent(OnAtmosAlarm); + + // Visuals + SubscribeLocalEvent(UpdateVisuals); + } + + #region Visuals + private void UpdateVisuals(EntityUid uid, FirelockComponent component, EntityEventArgs args) => UpdateVisuals(uid); + + public override void Update(float frameTime) + { + _accumulatedFrameTime += frameTime; + if (_accumulatedFrameTime < _visualUpdateInterval) + return; + + _accumulatedFrameTime -= _visualUpdateInterval; + + var powerQuery = GetEntityQuery(); + + foreach (var (_, door, appearance, xform) in EntityQuery()) + { + UpdateVisuals(door.Owner, door, appearance, powerQuery); + } + } + + private void UpdateVisuals(EntityUid uid, + DoorComponent? door = null, + AppearanceComponent? appearance = null, + EntityQuery? powerQuery = null) + { + if (!Resolve(uid, ref door, ref appearance, false)) + return; + + // only bother to check pressure on doors that are some variation of closed. + if (door.State != DoorState.Closed + && door.State != DoorState.Welded + && door.State != DoorState.Denying) + { + appearance.SetData(DoorVisuals.ClosedLights, false); + return; + } + + powerQuery ??= EntityManager.GetEntityQuery(); + if (powerQuery.Value.TryGetComponent(uid, out var receiver) && !receiver.Powered) + { + appearance.SetData(DoorVisuals.ClosedLights, false); + return; + } + + appearance.SetData(DoorVisuals.ClosedLights, + IsHoldingPressureOrFire(uid)); + } + #endregion + + public bool EmergencyPressureStop(EntityUid uid, FirelockComponent? firelock = null, DoorComponent? door = null) + { + if (!Resolve(uid, ref firelock, ref door)) + return false; + + if (door.State == DoorState.Open) + { + if (_doorSystem.TryClose(door.Owner, door)) + { + return _doorSystem.OnPartialClose(door.Owner, door); + } + } + return false; } private void OnBeforeDoorOpened(EntityUid uid, FirelockComponent component, BeforeDoorOpenedEvent args) { - if (!this.IsPowered(uid, EntityManager) || component.IsHoldingFire() || component.IsHoldingPressure()) + if (!this.IsPowered(uid, EntityManager) || IsHoldingPressureOrFire(uid)) args.Cancel(); } - private void OnBeforeDoorDenied(EntityUid uid, FirelockComponent component, BeforeDoorDeniedEvent args) - { - args.Cancel(); - } - private void OnDoorGetPryTimeModifier(EntityUid uid, FirelockComponent component, DoorGetPryTimeModifierEvent args) { - if (component.IsHoldingFire() || component.IsHoldingPressure()) + var state = CheckPressureAndFire(uid); + + if (state.Fire) + { + _popupSystem.PopupEntity(Loc.GetString("firelock-component-is-holding-fire-message"), + uid, Filter.Pvs(uid, entityManager: EntityManager)); + } + else if (state.Pressure) + { + _popupSystem.PopupEntity(Loc.GetString("firelock-component-is-holding-pressure-message"), + uid, Filter.Pvs(uid, entityManager: EntityManager)); + } + + if (state.Fire || state.Pressure) args.PryTimeModifier *= component.LockedPryTimeModifier; } - private void OnBeforeDoorPry(EntityUid uid, FirelockComponent component, BeforeDoorPryEvent args) - { - if (!TryComp(uid, out var door) || door.State != DoorState.Closed) - { - return; - } - - if (component.IsHoldingPressure()) - { - component.Owner.PopupMessage(args.User, Loc.GetString("firelock-component-is-holding-pressure-message")); - } - else if (component.IsHoldingFire()) - { - component.Owner.PopupMessage(args.User, Loc.GetString("firelock-component-is-holding-fire-message")); - } - } - private void OnUpdateState(EntityUid uid, FirelockComponent component, DoorStateChangedEvent args) { var ev = new BeforeDoorAutoCloseEvent(); @@ -73,6 +138,7 @@ namespace Content.Server.Doors.Systems } _doorSystem.SetNextStateChange(uid, component.AutocloseDelay); + UpdateVisuals(uid, component, args); } private void OnBeforeDoorAutoclose(EntityUid uid, FirelockComponent component, BeforeDoorAutoCloseEvent args) @@ -83,7 +149,8 @@ namespace Content.Server.Doors.Systems // Make firelocks autoclose, but only if the last alarm type it // remembers was a danger. This is to prevent people from // flooding hallways with endless bad air/fire. - if (_atmosAlarmable.TryGetHighestAlert(uid, out var alarm) && alarm != AtmosAlarmType.Danger || alarm == null) + if (component.AlarmAutoClose && + (_atmosAlarmable.TryGetHighestAlert(uid, out var alarm) && alarm != AtmosAlarmType.Danger || alarm == null)) args.Cancel(); } @@ -98,7 +165,58 @@ namespace Content.Server.Doors.Systems } else if (args.AlarmType == AtmosAlarmType.Danger) { - component.EmergencyPressureStop(); + EmergencyPressureStop(uid, component, doorComponent); + } + } + + public bool IsHoldingPressureOrFire(EntityUid uid) + { + var result = CheckPressureAndFire(uid); + return result.Pressure || result.Fire; + } + + public (bool Pressure, bool Fire) CheckPressureAndFire(EntityUid owner) + { + float threshold = 20; + var atmosphereSystem = EntityManager.EntitySysManager.GetEntitySystem(); + var transformSystem = EntityManager.EntitySysManager.GetEntitySystem(); + var transform = EntityManager.GetComponent(owner); + var position = transformSystem.GetGridOrMapTilePosition(owner, transform); + if (transform.GridUid is not {} gridUid) + return (false, false); + var minMoles = float.MaxValue; + var maxMoles = 0f; + + return (IsHoldingPressure(), IsHoldingFire()); + + bool IsHoldingPressure() + { + foreach (var adjacent in atmosphereSystem.GetAdjacentTileMixtures(gridUid, position)) + { + var moles = adjacent.TotalMoles; + if (moles < minMoles) + minMoles = moles; + if (moles > maxMoles) + maxMoles = moles; + } + + return (maxMoles - minMoles) > threshold; + } + + bool IsHoldingFire() + { + if (atmosphereSystem.GetTileMixture(gridUid, null, position) == null) + return false; + + if (atmosphereSystem.IsHotspotActive(gridUid, position)) + return true; + + foreach (var adjacent in atmosphereSystem.GetAdjacentTiles(gridUid, position)) + { + if (atmosphereSystem.IsHotspotActive(gridUid, adjacent)) + return true; + } + return false; } } } diff --git a/Content.Shared/Doors/Components/DoorComponent.cs b/Content.Shared/Doors/Components/DoorComponent.cs index 20376b23f8..b2f8a6b585 100644 --- a/Content.Shared/Doors/Components/DoorComponent.cs +++ b/Content.Shared/Doors/Components/DoorComponent.cs @@ -231,6 +231,7 @@ public enum DoorVisuals Powered, BoltLights, EmergencyLights, + ClosedLights, BaseRSI, } diff --git a/Resources/Textures/Structures/Doors/Airlocks/Glass/external.rsi/closed_unlit.png b/Resources/Textures/Structures/Doors/Airlocks/Glass/external.rsi/closed_unlit.png index 6e3cb09bcf..79125cbe2e 100644 Binary files a/Resources/Textures/Structures/Doors/Airlocks/Glass/external.rsi/closed_unlit.png and b/Resources/Textures/Structures/Doors/Airlocks/Glass/external.rsi/closed_unlit.png differ diff --git a/Resources/Textures/Structures/Doors/Airlocks/Glass/shuttle.rsi/bolted_unlit.png b/Resources/Textures/Structures/Doors/Airlocks/Glass/shuttle.rsi/bolted_unlit.png index 7c80bc210c..844bd201f1 100644 Binary files a/Resources/Textures/Structures/Doors/Airlocks/Glass/shuttle.rsi/bolted_unlit.png and b/Resources/Textures/Structures/Doors/Airlocks/Glass/shuttle.rsi/bolted_unlit.png differ diff --git a/Resources/Textures/Structures/Doors/Airlocks/Glass/shuttle.rsi/closed_unlit.png b/Resources/Textures/Structures/Doors/Airlocks/Glass/shuttle.rsi/closed_unlit.png index 844bd201f1..7c80bc210c 100644 Binary files a/Resources/Textures/Structures/Doors/Airlocks/Glass/shuttle.rsi/closed_unlit.png and b/Resources/Textures/Structures/Doors/Airlocks/Glass/shuttle.rsi/closed_unlit.png differ diff --git a/Resources/Textures/Structures/Doors/Airlocks/Standard/external.rsi/closed_unlit.png b/Resources/Textures/Structures/Doors/Airlocks/Standard/external.rsi/closed_unlit.png index 6e3cb09bcf..79125cbe2e 100644 Binary files a/Resources/Textures/Structures/Doors/Airlocks/Standard/external.rsi/closed_unlit.png and b/Resources/Textures/Structures/Doors/Airlocks/Standard/external.rsi/closed_unlit.png differ diff --git a/Resources/Textures/Structures/Doors/Airlocks/Standard/shuttle.rsi/bolted_unlit.png b/Resources/Textures/Structures/Doors/Airlocks/Standard/shuttle.rsi/bolted_unlit.png index 7c80bc210c..844bd201f1 100644 Binary files a/Resources/Textures/Structures/Doors/Airlocks/Standard/shuttle.rsi/bolted_unlit.png and b/Resources/Textures/Structures/Doors/Airlocks/Standard/shuttle.rsi/bolted_unlit.png differ diff --git a/Resources/Textures/Structures/Doors/Airlocks/Standard/shuttle.rsi/closed_unlit.png b/Resources/Textures/Structures/Doors/Airlocks/Standard/shuttle.rsi/closed_unlit.png index 844bd201f1..7c80bc210c 100644 Binary files a/Resources/Textures/Structures/Doors/Airlocks/Standard/shuttle.rsi/closed_unlit.png and b/Resources/Textures/Structures/Doors/Airlocks/Standard/shuttle.rsi/closed_unlit.png differ