Files
tbd-station-14/Content.Server/Doors/Systems/FirelockSystem.cs
nikthechampiongr df8b9a659e 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>
2024-05-22 04:16:20 -07:00

247 lines
10 KiB
C#

using Content.Server.Atmos.Components;
using Content.Server.Atmos.EntitySystems;
using Content.Server.Atmos.Monitor.Systems;
using Content.Server.Power.Components;
using Content.Server.Power.EntitySystems;
using Content.Server.Shuttles.Components;
using Content.Shared.Atmos;
using Content.Shared.Atmos.Monitor;
using Content.Shared.Doors;
using Content.Shared.Doors.Components;
using Content.Shared.Doors.Systems;
using Robust.Shared.Map.Components;
namespace Content.Server.Doors.Systems
{
public sealed class FirelockSystem : SharedFirelockSystem
{
[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 SharedMapSystem _mapping = default!;
private const int UpdateInterval = 30;
private int _accumulatedTicks;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<FirelockComponent, BeforeDoorAutoCloseEvent>(OnBeforeDoorAutoclose);
SubscribeLocalEvent<FirelockComponent, AtmosAlarmEvent>(OnAtmosAlarm);
SubscribeLocalEvent<FirelockComponent, PowerChangedEvent>(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);
}
public override void Update(float frameTime)
{
_accumulatedTicks += 1;
if (_accumulatedTicks < UpdateInterval)
return;
_accumulatedTicks = 0;
var airtightQuery = GetEntityQuery<AirtightComponent>();
var appearanceQuery = GetEntityQuery<AppearanceComponent>();
var xformQuery = GetEntityQuery<TransformComponent>();
var query = EntityQueryEnumerator<FirelockComponent, DoorComponent>();
while (query.MoveNext(out var uid, out var firelock, out var door))
{
// 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)
{
continue;
}
if (airtightQuery.TryGetComponent(uid, out var airtight)
&& xformQuery.TryGetComponent(uid, out var xform)
&& appearanceQuery.TryGetComponent(uid, out var appearance))
{
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 OnBeforeDoorAutoclose(EntityUid uid, FirelockComponent component, BeforeDoorAutoCloseEvent args)
{
if (!this.IsPowered(uid, EntityManager))
args.Cancel();
// 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 (component.AlarmAutoClose &&
(_atmosAlarmable.TryGetHighestAlert(uid, out var alarm) && alarm != AtmosAlarmType.Danger || alarm == null))
args.Cancel();
}
private void OnAtmosAlarm(EntityUid uid, FirelockComponent component, AtmosAlarmEvent args)
{
if (!this.IsPowered(uid, EntityManager))
return;
if (!TryComp<DoorComponent>(uid, out var doorComponent))
return;
if (args.AlarmType == AtmosAlarmType.Normal || args.AlarmType == AtmosAlarmType.Warning)
{
if (doorComponent.State == DoorState.Closed)
_doorSystem.TryOpen(uid);
}
else if (args.AlarmType == AtmosAlarmType.Danger)
{
EmergencyPressureStop(uid, component, doorComponent);
}
}
public (bool Pressure, bool Fire) CheckPressureAndFire(EntityUid uid, FirelockComponent firelock)
{
var query = GetEntityQuery<AirtightComponent>();
if (query.TryGetComponent(uid, out AirtightComponent? airtight))
return CheckPressureAndFire(uid, firelock, Transform(uid), airtight, query);
return (false, false);
}
public (bool Pressure, bool Fire) CheckPressureAndFire(
EntityUid uid,
FirelockComponent firelock,
TransformComponent xform,
AirtightComponent airtight,
EntityQuery<AirtightComponent> airtightQuery)
{
if (!airtight.AirBlocked)
return (false, false);
if (TryComp(uid, out DockingComponent? dock) && dock.Docked)
{
// Currently docking automatically opens the doors. But maybe in future, check the pressure difference before opening doors?
return (false, false);
}
if (!HasComp<GridAtmosphereComponent>(xform.ParentUid))
return (false, false);
var grid = Comp<MapGridComponent>(xform.ParentUid);
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;
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.
// GetAdjacentTileMixtures also ignores empty/non-existent tiles, which we don't want. Additionally, for
// edge-fire locks, we only want to enumerate over a single directions. So AFAIK there is no nice way of
// achieving all this using existing atmos functions, and the functionality is too specialized to bother
// adding new public atmos system functions.
List<Vector2i> tiles = new(4);
List<AtmosDirection> directions = new(4);
for (var i = 0; i < Atmospherics.Directions; i++)
{
var dir = (AtmosDirection)(1 << i);
if (airtight.AirBlockedDirection.HasFlag(dir))
{
directions.Add(dir);
tiles.Add(pos.Offset(dir));
}
}
// May also have to consider pressure on the same tile as the firelock.
var count = tiles.Count;
if (airtight.AirBlockedDirection != AtmosDirection.All)
tiles.Add(pos);
var gasses = _atmosSystem.GetTileMixtures(xform.ParentUid, xform.MapUid, tiles);
if (gasses == null)
return (false, false);
for (var i = 0; i < count; i++)
{
var gas = gasses[i];
var dir = directions[i];
var adjacentPos = tiles[i];
if (gas != null)
{
// Is there some airtight entity blocking this direction? If yes, don't include this direction in the
// pressure differential
if (HasAirtightBlocker(_mapping.GetAnchoredEntities(xform.ParentUid, grid, adjacentPos), dir.GetOpposite(), airtightQuery))
continue;
var p = gas.Pressure;
minPressure = Math.Min(minPressure, p);
maxPressure = Math.Max(maxPressure, p);
minTemperature = Math.Min(minTemperature, gas.Temperature);
maxTemperature = Math.Max(maxTemperature, gas.Temperature);
}
holdingPressure |= maxPressure - minPressure > firelock.PressureThreshold;
holdingFire |= maxTemperature - minTemperature > firelock.TemperatureThreshold;
if (holdingPressure && holdingFire)
return (holdingPressure, holdingFire);
}
if (airtight.AirBlockedDirection == AtmosDirection.All)
return (holdingPressure, holdingFire);
var local = gasses[count];
if (local != null)
{
var p = local.Pressure;
minPressure = Math.Min(minPressure, p);
maxPressure = Math.Max(maxPressure, p);
minTemperature = Math.Min(minTemperature, local.Temperature);
maxTemperature = Math.Max(maxTemperature, local.Temperature);
}
else
{
minPressure = Math.Min(minPressure, 0);
maxPressure = Math.Max(maxPressure, 0);
minTemperature = Math.Min(minTemperature, 0);
maxTemperature = Math.Max(maxTemperature, 0);
}
holdingPressure |= maxPressure - minPressure > firelock.PressureThreshold;
holdingFire |= maxTemperature - minTemperature > firelock.TemperatureThreshold;
return (holdingPressure, holdingFire);
}
private bool HasAirtightBlocker(IEnumerable<EntityUid> enumerable, AtmosDirection dir, EntityQuery<AirtightComponent> airtightQuery)
{
foreach (var ent in enumerable)
{
if (!airtightQuery.TryGetComponent(ent, out var airtight) || !airtight.AirBlocked)
continue;
if ((airtight.AirBlockedDirection & dir) == dir)
return true;
}
return false;
}
}
}