diff --git a/Content.Client/Doors/FirelockSystem.cs b/Content.Client/Doors/FirelockSystem.cs index cfd84a4713..f64b4c8e52 100644 --- a/Content.Client/Doors/FirelockSystem.cs +++ b/Content.Client/Doors/FirelockSystem.cs @@ -1,9 +1,10 @@ using Content.Shared.Doors.Components; +using Content.Shared.Doors.Systems; using Robust.Client.GameObjects; namespace Content.Client.Doors; -public sealed class FirelockSystem : EntitySystem +public sealed class FirelockSystem : SharedFirelockSystem { [Dependency] private readonly SharedAppearanceSystem _appearanceSystem = default!; diff --git a/Content.Server/Doors/Systems/FirelockSystem.cs b/Content.Server/Doors/Systems/FirelockSystem.cs index 3d4c8a4ec5..5ad86fb20a 100644 --- a/Content.Server/Doors/Systems/FirelockSystem.cs +++ b/Content.Server/Doors/Systems/FirelockSystem.cs @@ -1,67 +1,56 @@ using Content.Server.Atmos.Components; using Content.Server.Atmos.EntitySystems; using Content.Server.Atmos.Monitor.Systems; -using Content.Server.Popups; using Content.Server.Power.Components; using Content.Server.Power.EntitySystems; using Content.Server.Shuttles.Components; -using Content.Shared.Access.Systems; using Content.Shared.Atmos; using Content.Shared.Atmos.Monitor; using Content.Shared.Doors; using Content.Shared.Doors.Components; using Content.Shared.Doors.Systems; -using Content.Shared.Popups; -using Content.Shared.Prying.Components; using Robust.Shared.Map.Components; namespace Content.Server.Doors.Systems { - public sealed class FirelockSystem : EntitySystem + public sealed class FirelockSystem : SharedFirelockSystem { - [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 SharedAppearanceSystem _appearance = default!; - [Dependency] private readonly AccessReaderSystem _accessReaderSystem = default!; + [Dependency] private readonly SharedMapSystem _mapping = default!; - private static float _visualUpdateInterval = 0.5f; - private float _accumulatedFrameTime; + private const int UpdateInterval = 30; + private int _accumulatedTicks; public override void Initialize() { base.Initialize(); - SubscribeLocalEvent(OnBeforeDoorOpened); - SubscribeLocalEvent(OnDoorGetPryTimeModifier); - SubscribeLocalEvent(OnUpdateState); SubscribeLocalEvent(OnBeforeDoorAutoclose); SubscribeLocalEvent(OnAtmosAlarm); - // Visuals - SubscribeLocalEvent(UpdateVisuals); - SubscribeLocalEvent(UpdateVisuals); SubscribeLocalEvent(PowerChanged); + } private void PowerChanged(EntityUid uid, FirelockComponent component, ref PowerChangedEvent args) { // TODO this should REALLLLY not be door specific appearance thing. _appearance.SetData(uid, DoorVisuals.Powered, args.Powered); + component.Powered = args.Powered; + Dirty(uid, component); } - #region Visuals - private void UpdateVisuals(EntityUid uid, FirelockComponent component, EntityEventArgs args) => UpdateVisuals(uid, component); - public override void Update(float frameTime) { - _accumulatedFrameTime += frameTime; - if (_accumulatedFrameTime < _visualUpdateInterval) + _accumulatedTicks += 1; + if (_accumulatedTicks < UpdateInterval) return; - _accumulatedFrameTime -= _visualUpdateInterval; + _accumulatedTicks = 0; var airtightQuery = GetEntityQuery(); var appearanceQuery = GetEntityQuery(); @@ -84,94 +73,13 @@ namespace Content.Server.Doors.Systems { var (fire, pressure) = CheckPressureAndFire(uid, firelock, xform, airtight, airtightQuery); _appearance.SetData(uid, DoorVisuals.ClosedLights, fire || pressure, appearance); + firelock.Temperature = fire; + firelock.Pressure = pressure; + Dirty(uid, firelock); } } } - private void UpdateVisuals(EntityUid uid, - FirelockComponent? firelock = null, - DoorComponent? door = null, - AirtightComponent? airtight = null, - AppearanceComponent? appearance = null, - TransformComponent? xform = 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(uid, DoorVisuals.ClosedLights, false, appearance); - return; - } - - var query = GetEntityQuery(); - if (!Resolve(uid, ref firelock, ref airtight, ref appearance, ref xform, false) || !query.Resolve(uid, ref airtight, false)) - return; - - var (fire, pressure) = CheckPressureAndFire(uid, firelock, xform, airtight, query); - _appearance.SetData(uid, DoorVisuals.ClosedLights, fire || pressure, appearance); - } - #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(uid, door)) - { - return _doorSystem.OnPartialClose(uid, door); - } - } - return false; - } - - private void OnBeforeDoorOpened(EntityUid uid, FirelockComponent component, BeforeDoorOpenedEvent args) - { - // Give the Door remote the ability to force a firelock open even if it is holding back dangerous gas - var overrideAccess = (args.User != null) && _accessReaderSystem.IsAllowed(args.User.Value, uid); - - if (!this.IsPowered(uid, EntityManager) || (!overrideAccess && IsHoldingPressureOrFire(uid, component))) - args.Cancel(); - } - - private void OnDoorGetPryTimeModifier(EntityUid uid, FirelockComponent component, ref GetPryTimeModifierEvent args) - { - var state = CheckPressureAndFire(uid, component); - - if (state.Fire) - { - _popupSystem.PopupEntity(Loc.GetString("firelock-component-is-holding-fire-message"), - uid, args.User, PopupType.MediumCaution); - } - else if (state.Pressure) - { - _popupSystem.PopupEntity(Loc.GetString("firelock-component-is-holding-pressure-message"), - uid, args.User, PopupType.MediumCaution); - } - - if (state.Fire || state.Pressure) - args.PryTimeModifier *= component.LockedPryTimeModifier; - } - - private void OnUpdateState(EntityUid uid, FirelockComponent component, DoorStateChangedEvent args) - { - var ev = new BeforeDoorAutoCloseEvent(); - RaiseLocalEvent(uid, ev); - UpdateVisuals(uid, component, args); - if (ev.Cancelled) - { - return; - } - - _doorSystem.SetNextStateChange(uid, component.AutocloseDelay); - } - private void OnBeforeDoorAutoclose(EntityUid uid, FirelockComponent component, BeforeDoorAutoCloseEvent args) { if (!this.IsPowered(uid, EntityManager)) @@ -204,12 +112,6 @@ namespace Content.Server.Doors.Systems } } - public bool IsHoldingPressureOrFire(EntityUid uid, FirelockComponent firelock) - { - var result = CheckPressureAndFire(uid, firelock); - return result.Pressure || result.Fire; - } - public (bool Pressure, bool Fire) CheckPressureAndFire(EntityUid uid, FirelockComponent firelock) { var query = GetEntityQuery(); @@ -234,17 +136,17 @@ namespace Content.Server.Doors.Systems return (false, false); } - if (!TryComp(xform.ParentUid, out GridAtmosphereComponent? gridAtmosphere)) + if (!HasComp(xform.ParentUid)) return (false, false); var grid = Comp(xform.ParentUid); - var pos = grid.CoordinatesToTile(xform.Coordinates); + var pos = _mapping.CoordinatesToTile(xform.ParentUid, grid, xform.Coordinates); var minPressure = float.MaxValue; var maxPressure = float.MinValue; var minTemperature = float.MaxValue; var maxTemperature = float.MinValue; - bool holdingFire = false; - bool holdingPressure = false; + var holdingFire = false; + var holdingPressure = false; // We cannot simply use `_atmosSystem.GetAdjacentTileMixtures` because of how the `includeBlocked` option // works, we want to ignore the firelock's blocking, while including blockers on other tiles. @@ -284,7 +186,7 @@ namespace Content.Server.Doors.Systems { // Is there some airtight entity blocking this direction? If yes, don't include this direction in the // pressure differential - if (HasAirtightBlocker(grid.GetAnchoredEntities(adjacentPos), dir.GetOpposite(), airtightQuery)) + if (HasAirtightBlocker(_mapping.GetAnchoredEntities(xform.ParentUid, grid, adjacentPos), dir.GetOpposite(), airtightQuery)) continue; var p = gas.Pressure; diff --git a/Content.Shared/Doors/Components/FirelockComponent.cs b/Content.Shared/Doors/Components/FirelockComponent.cs index 97e57185ca..ca62daaa0f 100644 --- a/Content.Shared/Doors/Components/FirelockComponent.cs +++ b/Content.Shared/Doors/Components/FirelockComponent.cs @@ -1,4 +1,4 @@ -using Content.Shared.Doors.Components; +using Robust.Shared.GameStates; namespace Content.Shared.Doors.Components { @@ -7,9 +7,11 @@ namespace Content.Shared.Doors.Components /// auto-closing on depressurization, air/fire alarm interactions, and preventing normal door functions when /// retaining pressure.. /// - [RegisterComponent] + [RegisterComponent, NetworkedComponent, AutoGenerateComponentState] public sealed partial class FirelockComponent : Component { + #region Settings + /// /// Pry time modifier to be used when the firelock is currently closed due to fire or pressure. /// @@ -39,5 +41,47 @@ namespace Content.Shared.Doors.Components /// [DataField("alarmAutoClose"), ViewVariables(VVAccess.ReadWrite)] public bool AlarmAutoClose = true; + + /// + /// The cooldown duration before a firelock can automatically close due to a hazardous environment after it has + /// been pried open. Measured in seconds. + /// + [DataField] + public TimeSpan EmergencyCloseCooldownDuration = TimeSpan.FromSeconds(2); + + #endregion + + #region Set by system + + /// + /// When the firelock will be allowed to automatically close again due to a hazardous environment. + /// + [DataField] + public TimeSpan? EmergencyCloseCooldown; + + /// + /// Whether the firelock can open, or is locked due to its environment. + /// + public bool IsLocked => Pressure || Temperature; + + /// + /// Whether the firelock is holding back a hazardous pressure. + /// + [DataField, AutoNetworkedField] + public bool Pressure; + + /// + /// Whether the firelock is holding back extreme temperatures. + /// + [DataField, AutoNetworkedField] + public bool Temperature; + + /// + /// Whether the airlock is powered. + /// + [DataField, AutoNetworkedField] + public bool Powered; + + #endregion } } diff --git a/Content.Shared/Doors/Systems/SharedDoorSystem.cs b/Content.Shared/Doors/Systems/SharedDoorSystem.cs index b58b7b265e..20456c1477 100644 --- a/Content.Shared/Doors/Systems/SharedDoorSystem.cs +++ b/Content.Shared/Doors/Systems/SharedDoorSystem.cs @@ -6,7 +6,6 @@ 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; @@ -466,7 +465,7 @@ public abstract partial class SharedDoorSystem : EntitySystem door.Partial = true; - // Make sure no entity waled into the airlock when it started closing. + // Make sure no entity walked into the airlock when it started closing. if (!CanClose(uid, door)) { door.NextStateChange = GameTiming.CurTime + door.OpenTimeTwo; diff --git a/Content.Shared/Doors/Systems/SharedFirelockSystem.cs b/Content.Shared/Doors/Systems/SharedFirelockSystem.cs new file mode 100644 index 0000000000..7d033efdd4 --- /dev/null +++ b/Content.Shared/Doors/Systems/SharedFirelockSystem.cs @@ -0,0 +1,125 @@ +using Content.Shared.Access.Systems; +using Content.Shared.Doors.Components; +using Content.Shared.Popups; +using Content.Shared.Prying.Components; +using Robust.Shared.Timing; + +namespace Content.Shared.Doors.Systems; + +public abstract class SharedFirelockSystem : EntitySystem +{ + [Dependency] private readonly AccessReaderSystem _accessReaderSystem = default!; + [Dependency] private readonly SharedPopupSystem _popupSystem = default!; + [Dependency] private readonly SharedAppearanceSystem _appearance = default!; + [Dependency] private readonly SharedDoorSystem _doorSystem = default!; + [Dependency] private readonly IGameTiming _gameTiming = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnUpdateState); + + // Access/Prying + SubscribeLocalEvent(OnBeforeDoorOpened); + SubscribeLocalEvent(OnDoorGetPryTimeModifier); + SubscribeLocalEvent(OnAfterPried); + + // Visuals + SubscribeLocalEvent(UpdateVisuals); + SubscribeLocalEvent(UpdateVisuals); + } + + 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 + || firelock.EmergencyCloseCooldown != null + && _gameTiming.CurTime < firelock.EmergencyCloseCooldown) + return false; + + if (!_doorSystem.TryClose(uid, door)) + return false; + + return _doorSystem.OnPartialClose(uid, door); + } + + private void OnUpdateState(EntityUid uid, FirelockComponent component, DoorStateChangedEvent args) + { + var ev = new BeforeDoorAutoCloseEvent(); + RaiseLocalEvent(uid, ev); + UpdateVisuals(uid, component, args); + if (ev.Cancelled) + { + return; + } + + _doorSystem.SetNextStateChange(uid, component.AutocloseDelay); + } + + #region Access/Prying + + private void OnBeforeDoorOpened(EntityUid uid, FirelockComponent component, BeforeDoorOpenedEvent args) + { + // Give the Door remote the ability to force a firelock open even if it is holding back dangerous gas + var overrideAccess = (args.User != null) && _accessReaderSystem.IsAllowed(args.User.Value, uid); + + if (!component.Powered || (!overrideAccess && component.IsLocked)) + args.Cancel(); + } + + private void OnDoorGetPryTimeModifier(EntityUid uid, FirelockComponent component, ref GetPryTimeModifierEvent args) + { + if (component.Temperature) + { + _popupSystem.PopupClient(Loc.GetString("firelock-component-is-holding-fire-message"), + uid, args.User, PopupType.MediumCaution); + } + else if (component.Pressure) + { + _popupSystem.PopupClient(Loc.GetString("firelock-component-is-holding-pressure-message"), + uid, args.User, PopupType.MediumCaution); + } + + if (component.IsLocked) + args.PryTimeModifier *= component.LockedPryTimeModifier; + } + + private void OnAfterPried(EntityUid uid, FirelockComponent component, ref PriedEvent args) + { + component.EmergencyCloseCooldown = _gameTiming.CurTime + component.EmergencyCloseCooldownDuration; + } + + #endregion + + #region Visuals + + private void UpdateVisuals(EntityUid uid, FirelockComponent component, EntityEventArgs args) => UpdateVisuals(uid, component); + + private void UpdateVisuals(EntityUid uid, + FirelockComponent? firelock = null, + DoorComponent? door = null, + AppearanceComponent? appearance = 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(uid, DoorVisuals.ClosedLights, false, appearance); + return; + } + + if (!Resolve(uid, ref firelock, ref appearance, false)) + return; + + _appearance.SetData(uid, DoorVisuals.ClosedLights, firelock.IsLocked, appearance); + } + + #endregion +} diff --git a/Content.Shared/Prying/Components/PryUnpoweredComponent.cs b/Content.Shared/Prying/Components/PryUnpoweredComponent.cs index f0e61dc968..b6b69a2577 100644 --- a/Content.Shared/Prying/Components/PryUnpoweredComponent.cs +++ b/Content.Shared/Prying/Components/PryUnpoweredComponent.cs @@ -8,4 +8,6 @@ namespace Content.Shared.Prying.Components; [RegisterComponent, NetworkedComponent] public sealed partial class PryUnpoweredComponent : Component { + [DataField] + public float PryModifier = 0.1f; } diff --git a/Content.Shared/Prying/Systems/PryingSystem.cs b/Content.Shared/Prying/Systems/PryingSystem.cs index ab87585c70..372c89c9ae 100644 --- a/Content.Shared/Prying/Systems/PryingSystem.cs +++ b/Content.Shared/Prying/Systems/PryingSystem.cs @@ -93,17 +93,17 @@ public sealed class PryingSystem : EntitySystem id = null; // We don't care about displaying a message if no tool was used. - if (!CanPry(target, user, out _)) + if (!TryComp(target, out var unpoweredComp) || !CanPry(target, user, out _, unpoweredComp: unpoweredComp)) // If we have reached this point we want the event that caused this // to be marked as handled. return true; // hand-prying is much slower - var modifier = CompOrNull(user)?.SpeedModifier ?? 0.1f; + var modifier = CompOrNull(user)?.SpeedModifier ?? unpoweredComp.PryModifier; return StartPry(target, user, null, modifier, out id); } - private bool CanPry(EntityUid target, EntityUid user, out string? message, PryingComponent? comp = null) + private bool CanPry(EntityUid target, EntityUid user, out string? message, PryingComponent? comp = null, PryUnpoweredComponent? unpoweredComp = null) { BeforePryEvent canev; @@ -113,7 +113,7 @@ public sealed class PryingSystem : EntitySystem } else { - if (!TryComp(target, out _)) + if (!Resolve(target, ref unpoweredComp)) { message = null; return false; diff --git a/Resources/Prototypes/Entities/Structures/Doors/Firelocks/firelock.yml b/Resources/Prototypes/Entities/Structures/Doors/Firelocks/firelock.yml index fcdb432dce..860db862ae 100644 --- a/Resources/Prototypes/Entities/Structures/Doors/Firelocks/firelock.yml +++ b/Resources/Prototypes/Entities/Structures/Doors/Firelocks/firelock.yml @@ -108,6 +108,8 @@ - type: DoorBolt - type: AccessReader access: [ [ "Engineering" ] ] + - type: PryUnpowered + pryModifier: 0.5 - type: entity id: Firelock