Firelock improvements part 1 (#26582)

* Change prying system and pryunpoweredcomp to allow for custom time modifiers

This will be useful if I go the route of making firelocks pryable when
unpowered instead of just being able to open and close instantly when
unpowered.

* Make firelocks properly predicted

Shared system made. Since atmos checks can only be done on the server we
just have it set relevant bools on the component and then dirty it.
Ditched atmos checks on trying to open, they now only happen whenever
firelocks are updated.

* Make firelocks pryable without a crowbar

While this usually would only allow you to do this when a door is
unpowered, firelocks do not have the airlock component which actually
does that check. As such firelocks will always allow you to pry them
open/closed by hand.

* Clean up System. Change update interval to be based on ticks. Move as much as possible to shared

* Make firelocks unable to emergency close for 2 seconds after being pried open

* Clean up

* More cleanup

* Reorganize SharedFirelockSystem methods to match Initialize order

---------

Co-authored-by: ShadowCommander <10494922+ShadowCommander@users.noreply.github.com>
This commit is contained in:
nikthechampiongr
2024-05-22 11:16:20 +00:00
committed by GitHub
parent 6a9139b4e1
commit df8b9a659e
8 changed files with 200 additions and 125 deletions

View File

@@ -1,9 +1,10 @@
using Content.Shared.Doors.Components; using Content.Shared.Doors.Components;
using Content.Shared.Doors.Systems;
using Robust.Client.GameObjects; using Robust.Client.GameObjects;
namespace Content.Client.Doors; namespace Content.Client.Doors;
public sealed class FirelockSystem : EntitySystem public sealed class FirelockSystem : SharedFirelockSystem
{ {
[Dependency] private readonly SharedAppearanceSystem _appearanceSystem = default!; [Dependency] private readonly SharedAppearanceSystem _appearanceSystem = default!;

View File

@@ -1,67 +1,56 @@
using Content.Server.Atmos.Components; using Content.Server.Atmos.Components;
using Content.Server.Atmos.EntitySystems; using Content.Server.Atmos.EntitySystems;
using Content.Server.Atmos.Monitor.Systems; using Content.Server.Atmos.Monitor.Systems;
using Content.Server.Popups;
using Content.Server.Power.Components; using Content.Server.Power.Components;
using Content.Server.Power.EntitySystems; using Content.Server.Power.EntitySystems;
using Content.Server.Shuttles.Components; using Content.Server.Shuttles.Components;
using Content.Shared.Access.Systems;
using Content.Shared.Atmos; using Content.Shared.Atmos;
using Content.Shared.Atmos.Monitor; using Content.Shared.Atmos.Monitor;
using Content.Shared.Doors; using Content.Shared.Doors;
using Content.Shared.Doors.Components; using Content.Shared.Doors.Components;
using Content.Shared.Doors.Systems; using Content.Shared.Doors.Systems;
using Content.Shared.Popups;
using Content.Shared.Prying.Components;
using Robust.Shared.Map.Components; using Robust.Shared.Map.Components;
namespace Content.Server.Doors.Systems 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 SharedDoorSystem _doorSystem = default!;
[Dependency] private readonly AtmosAlarmableSystem _atmosAlarmable = default!; [Dependency] private readonly AtmosAlarmableSystem _atmosAlarmable = default!;
[Dependency] private readonly AtmosphereSystem _atmosSystem = default!; [Dependency] private readonly AtmosphereSystem _atmosSystem = default!;
[Dependency] private readonly SharedAppearanceSystem _appearance = 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 const int UpdateInterval = 30;
private float _accumulatedFrameTime; private int _accumulatedTicks;
public override void Initialize() public override void Initialize()
{ {
base.Initialize(); base.Initialize();
SubscribeLocalEvent<FirelockComponent, BeforeDoorOpenedEvent>(OnBeforeDoorOpened);
SubscribeLocalEvent<FirelockComponent, GetPryTimeModifierEvent>(OnDoorGetPryTimeModifier);
SubscribeLocalEvent<FirelockComponent, DoorStateChangedEvent>(OnUpdateState);
SubscribeLocalEvent<FirelockComponent, BeforeDoorAutoCloseEvent>(OnBeforeDoorAutoclose); SubscribeLocalEvent<FirelockComponent, BeforeDoorAutoCloseEvent>(OnBeforeDoorAutoclose);
SubscribeLocalEvent<FirelockComponent, AtmosAlarmEvent>(OnAtmosAlarm); SubscribeLocalEvent<FirelockComponent, AtmosAlarmEvent>(OnAtmosAlarm);
// Visuals
SubscribeLocalEvent<FirelockComponent, MapInitEvent>(UpdateVisuals);
SubscribeLocalEvent<FirelockComponent, ComponentStartup>(UpdateVisuals);
SubscribeLocalEvent<FirelockComponent, PowerChangedEvent>(PowerChanged); SubscribeLocalEvent<FirelockComponent, PowerChangedEvent>(PowerChanged);
} }
private void PowerChanged(EntityUid uid, FirelockComponent component, ref PowerChangedEvent args) private void PowerChanged(EntityUid uid, FirelockComponent component, ref PowerChangedEvent args)
{ {
// TODO this should REALLLLY not be door specific appearance thing. // TODO this should REALLLLY not be door specific appearance thing.
_appearance.SetData(uid, DoorVisuals.Powered, args.Powered); _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) public override void Update(float frameTime)
{ {
_accumulatedFrameTime += frameTime; _accumulatedTicks += 1;
if (_accumulatedFrameTime < _visualUpdateInterval) if (_accumulatedTicks < UpdateInterval)
return; return;
_accumulatedFrameTime -= _visualUpdateInterval; _accumulatedTicks = 0;
var airtightQuery = GetEntityQuery<AirtightComponent>(); var airtightQuery = GetEntityQuery<AirtightComponent>();
var appearanceQuery = GetEntityQuery<AppearanceComponent>(); var appearanceQuery = GetEntityQuery<AppearanceComponent>();
@@ -84,94 +73,13 @@ namespace Content.Server.Doors.Systems
{ {
var (fire, pressure) = CheckPressureAndFire(uid, firelock, xform, airtight, airtightQuery); var (fire, pressure) = CheckPressureAndFire(uid, firelock, xform, airtight, airtightQuery);
_appearance.SetData(uid, DoorVisuals.ClosedLights, fire || pressure, appearance); _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<AirtightComponent>();
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) private void OnBeforeDoorAutoclose(EntityUid uid, FirelockComponent component, BeforeDoorAutoCloseEvent args)
{ {
if (!this.IsPowered(uid, EntityManager)) 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) public (bool Pressure, bool Fire) CheckPressureAndFire(EntityUid uid, FirelockComponent firelock)
{ {
var query = GetEntityQuery<AirtightComponent>(); var query = GetEntityQuery<AirtightComponent>();
@@ -234,17 +136,17 @@ namespace Content.Server.Doors.Systems
return (false, false); return (false, false);
} }
if (!TryComp(xform.ParentUid, out GridAtmosphereComponent? gridAtmosphere)) if (!HasComp<GridAtmosphereComponent>(xform.ParentUid))
return (false, false); return (false, false);
var grid = Comp<MapGridComponent>(xform.ParentUid); var grid = Comp<MapGridComponent>(xform.ParentUid);
var pos = grid.CoordinatesToTile(xform.Coordinates); var pos = _mapping.CoordinatesToTile(xform.ParentUid, grid, xform.Coordinates);
var minPressure = float.MaxValue; var minPressure = float.MaxValue;
var maxPressure = float.MinValue; var maxPressure = float.MinValue;
var minTemperature = float.MaxValue; var minTemperature = float.MaxValue;
var maxTemperature = float.MinValue; var maxTemperature = float.MinValue;
bool holdingFire = false; var holdingFire = false;
bool holdingPressure = false; var holdingPressure = false;
// We cannot simply use `_atmosSystem.GetAdjacentTileMixtures` because of how the `includeBlocked` option // 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. // 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 // Is there some airtight entity blocking this direction? If yes, don't include this direction in the
// pressure differential // pressure differential
if (HasAirtightBlocker(grid.GetAnchoredEntities(adjacentPos), dir.GetOpposite(), airtightQuery)) if (HasAirtightBlocker(_mapping.GetAnchoredEntities(xform.ParentUid, grid, adjacentPos), dir.GetOpposite(), airtightQuery))
continue; continue;
var p = gas.Pressure; var p = gas.Pressure;

View File

@@ -1,4 +1,4 @@
using Content.Shared.Doors.Components; using Robust.Shared.GameStates;
namespace Content.Shared.Doors.Components 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 /// auto-closing on depressurization, air/fire alarm interactions, and preventing normal door functions when
/// retaining pressure.. /// retaining pressure..
/// </summary> /// </summary>
[RegisterComponent] [RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
public sealed partial class FirelockComponent : Component public sealed partial class FirelockComponent : Component
{ {
#region Settings
/// <summary> /// <summary>
/// Pry time modifier to be used when the firelock is currently closed due to fire or pressure. /// Pry time modifier to be used when the firelock is currently closed due to fire or pressure.
/// </summary> /// </summary>
@@ -39,5 +41,47 @@ namespace Content.Shared.Doors.Components
/// </summary> /// </summary>
[DataField("alarmAutoClose"), ViewVariables(VVAccess.ReadWrite)] [DataField("alarmAutoClose"), ViewVariables(VVAccess.ReadWrite)]
public bool AlarmAutoClose = true; public bool AlarmAutoClose = true;
/// <summary>
/// The cooldown duration before a firelock can automatically close due to a hazardous environment after it has
/// been pried open. Measured in seconds.
/// </summary>
[DataField]
public TimeSpan EmergencyCloseCooldownDuration = TimeSpan.FromSeconds(2);
#endregion
#region Set by system
/// <summary>
/// When the firelock will be allowed to automatically close again due to a hazardous environment.
/// </summary>
[DataField]
public TimeSpan? EmergencyCloseCooldown;
/// <summary>
/// Whether the firelock can open, or is locked due to its environment.
/// </summary>
public bool IsLocked => Pressure || Temperature;
/// <summary>
/// Whether the firelock is holding back a hazardous pressure.
/// </summary>
[DataField, AutoNetworkedField]
public bool Pressure;
/// <summary>
/// Whether the firelock is holding back extreme temperatures.
/// </summary>
[DataField, AutoNetworkedField]
public bool Temperature;
/// <summary>
/// Whether the airlock is powered.
/// </summary>
[DataField, AutoNetworkedField]
public bool Powered;
#endregion
} }
} }

View File

@@ -6,7 +6,6 @@ using Content.Shared.Damage;
using Content.Shared.Database; using Content.Shared.Database;
using Content.Shared.Doors.Components; using Content.Shared.Doors.Components;
using Content.Shared.Emag.Systems; using Content.Shared.Emag.Systems;
using Content.Shared.Hands.Components;
using Content.Shared.Interaction; using Content.Shared.Interaction;
using Content.Shared.Physics; using Content.Shared.Physics;
using Content.Shared.Popups; using Content.Shared.Popups;
@@ -466,7 +465,7 @@ public abstract partial class SharedDoorSystem : EntitySystem
door.Partial = true; 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)) if (!CanClose(uid, door))
{ {
door.NextStateChange = GameTiming.CurTime + door.OpenTimeTwo; door.NextStateChange = GameTiming.CurTime + door.OpenTimeTwo;

View File

@@ -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<FirelockComponent, DoorStateChangedEvent>(OnUpdateState);
// Access/Prying
SubscribeLocalEvent<FirelockComponent, BeforeDoorOpenedEvent>(OnBeforeDoorOpened);
SubscribeLocalEvent<FirelockComponent, GetPryTimeModifierEvent>(OnDoorGetPryTimeModifier);
SubscribeLocalEvent<FirelockComponent, PriedEvent>(OnAfterPried);
// Visuals
SubscribeLocalEvent<FirelockComponent, MapInitEvent>(UpdateVisuals);
SubscribeLocalEvent<FirelockComponent, ComponentStartup>(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
}

View File

@@ -8,4 +8,6 @@ namespace Content.Shared.Prying.Components;
[RegisterComponent, NetworkedComponent] [RegisterComponent, NetworkedComponent]
public sealed partial class PryUnpoweredComponent : Component public sealed partial class PryUnpoweredComponent : Component
{ {
[DataField]
public float PryModifier = 0.1f;
} }

View File

@@ -93,17 +93,17 @@ public sealed class PryingSystem : EntitySystem
id = null; id = null;
// We don't care about displaying a message if no tool was used. // We don't care about displaying a message if no tool was used.
if (!CanPry(target, user, out _)) if (!TryComp<PryUnpoweredComponent>(target, out var unpoweredComp) || !CanPry(target, user, out _, unpoweredComp: unpoweredComp))
// If we have reached this point we want the event that caused this // If we have reached this point we want the event that caused this
// to be marked as handled. // to be marked as handled.
return true; return true;
// hand-prying is much slower // hand-prying is much slower
var modifier = CompOrNull<PryingComponent>(user)?.SpeedModifier ?? 0.1f; var modifier = CompOrNull<PryingComponent>(user)?.SpeedModifier ?? unpoweredComp.PryModifier;
return StartPry(target, user, null, modifier, out id); 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; BeforePryEvent canev;
@@ -113,7 +113,7 @@ public sealed class PryingSystem : EntitySystem
} }
else else
{ {
if (!TryComp<PryUnpoweredComponent>(target, out _)) if (!Resolve(target, ref unpoweredComp))
{ {
message = null; message = null;
return false; return false;

View File

@@ -108,6 +108,8 @@
- type: DoorBolt - type: DoorBolt
- type: AccessReader - type: AccessReader
access: [ [ "Engineering" ] ] access: [ [ "Engineering" ] ]
- type: PryUnpowered
pryModifier: 0.5
- type: entity - type: entity
id: Firelock id: Firelock