Airlock / firelock code refactor, pseudo-prediction implementation (#3037)

* splits off airlocks, firelocks

* adds airlock prediction. anims broken though

* fixes animation weirdness

* removes opacity prediction because it looked odd

* Now firelocks don't visually start open. Argh.

* Fixes firelock weirdness, saneifies _state var.

* Documentation changes, code shuffle.

* Lets firelocks crush people.

* Stops open-hand opening/closing firelocks.

* updates serializable, netserializable attributes

* Addresses reviews... hopefully.

* updates submodule?

* nullability

* fuck fuck fuck fuck

* fucking finally
This commit is contained in:
tmtmtl30
2021-02-12 07:02:14 -08:00
committed by GitHub
parent 5f15d97940
commit 258fdc10ea
17 changed files with 1248 additions and 733 deletions

View File

@@ -1,4 +1,5 @@
using System;
#nullable enable
using System;
using Content.Client.GameObjects.Components.Wires;
using Content.Shared.Audio;
using Content.Shared.GameObjects.Components.Doors;
@@ -16,9 +17,9 @@ namespace Content.Client.GameObjects.Components.Doors
{
private const string AnimationKey = "airlock_animation";
private Animation CloseAnimation;
private Animation OpenAnimation;
private Animation DenyAnimation;
private Animation CloseAnimation = default!;
private Animation OpenAnimation = default!;
private Animation DenyAnimation = default!;
public override void LoadData(YamlMappingNode node)
{
@@ -113,35 +114,31 @@ namespace Content.Client.GameObjects.Components.Doors
var unlitVisible = true;
var boltedVisible = false;
var weldedVisible = false;
if (animPlayer.HasRunningAnimation(AnimationKey))
{
animPlayer.Stop(AnimationKey);
}
switch (state)
{
case DoorVisualState.Open:
sprite.LayerSetState(DoorVisualLayers.Base, "open");
unlitVisible = false;
break;
case DoorVisualState.Closed:
sprite.LayerSetState(DoorVisualLayers.Base, "closed");
sprite.LayerSetState(DoorVisualLayers.BaseUnlit, "closed_unlit");
sprite.LayerSetState(DoorVisualLayers.BaseBolted, "bolted");
sprite.LayerSetState(WiresVisualizer.WiresVisualLayers.MaintenancePanel, "panel_open");
break;
case DoorVisualState.Closing:
if (!animPlayer.HasRunningAnimation(AnimationKey))
{
animPlayer.Play(CloseAnimation, AnimationKey);
}
break;
case DoorVisualState.Opening:
if (!animPlayer.HasRunningAnimation(AnimationKey))
{
animPlayer.Play(OpenAnimation, AnimationKey);
}
break;
case DoorVisualState.Open:
sprite.LayerSetState(DoorVisualLayers.Base, "open");
unlitVisible = false;
case DoorVisualState.Closing:
animPlayer.Play(CloseAnimation, AnimationKey);
break;
case DoorVisualState.Deny:
if (!animPlayer.HasRunningAnimation(AnimationKey))
{
animPlayer.Play(DenyAnimation, AnimationKey);
}
break;
case DoorVisualState.Welded:
weldedVisible = true;

View File

@@ -0,0 +1,102 @@
#nullable enable
using Content.Shared.GameObjects.Components.Doors;
using JetBrains.Annotations;
using Robust.Shared.GameObjects;
using Robust.Shared.ViewVariables;
using System;
namespace Content.Client.GameObjects.Components.Doors
{
/// <summary>
/// Bare-bones client-side door component; used to stop door-based mispredicts.
/// </summary>
[UsedImplicitly]
[RegisterComponent]
[ComponentReference(typeof(SharedDoorComponent))]
public class ClientDoorComponent : SharedDoorComponent
{
private bool _stateChangeHasProgressed = false;
private TimeSpan _timeOffset;
public override DoorState State
{
protected set
{
if (State == value)
{
return;
}
base.State = value;
Owner.EntityManager.EventBus.RaiseEvent(EventSource.Local, new DoorStateMessage(this, State));
}
}
public override void HandleComponentState(ComponentState? curState, ComponentState? nextState)
{
base.HandleComponentState(curState, nextState);
if (curState is not DoorComponentState doorCompState)
{
return;
}
CurrentlyCrushing = doorCompState.CurrentlyCrushing;
StateChangeStartTime = doorCompState.StartTime;
State = doorCompState.DoorState;
if (StateChangeStartTime == null)
{
return;
}
_timeOffset = State switch
{
DoorState.Opening => OpenTimeOne,
DoorState.Closing => CloseTimeOne,
_ => throw new ArgumentOutOfRangeException(),
};
if (doorCompState.CurTime >= StateChangeStartTime + _timeOffset)
{
_stateChangeHasProgressed = true;
return;
}
_stateChangeHasProgressed = false;
}
public void OnUpdate()
{
if (!_stateChangeHasProgressed)
{
if (GameTiming.CurTime < StateChangeStartTime + _timeOffset) return;
if (State == DoorState.Opening)
{
OnPartialOpen();
}
else
{
OnPartialClose();
}
_stateChangeHasProgressed = true;
Dirty();
}
}
}
public sealed class DoorStateMessage : EntitySystemMessage
{
public ClientDoorComponent Component { get; }
public SharedDoorComponent.DoorState State { get; }
public DoorStateMessage(ClientDoorComponent component, SharedDoorComponent.DoorState state)
{
Component = component;
State = state;
}
}
}

View File

@@ -0,0 +1,62 @@
#nullable enable
using System;
using System.Collections.Generic;
using Content.Client.GameObjects.Components.Doors;
using Content.Shared.GameObjects.Components.Doors;
using Robust.Shared.GameObjects;
namespace Content.Client.GameObjects.EntitySystems
{
/// <summary>
/// Used by the client to "predict" when doors will change how collideable they are as part of their opening / closing.
/// </summary>
public class ClientDoorSystem : EntitySystem
{
/// <summary>
/// List of doors that need to be periodically checked.
/// </summary>
private readonly List<ClientDoorComponent> _activeDoors = new();
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<DoorStateMessage>(HandleDoorState);
}
/// <summary>
/// Registers doors to be periodically checked.
/// </summary>
/// <param name="message">A message corresponding to the component under consideration, raised when its state changes.</param>
private void HandleDoorState(DoorStateMessage message)
{
switch (message.State)
{
case SharedDoorComponent.DoorState.Closed:
case SharedDoorComponent.DoorState.Open:
_activeDoors.Remove(message.Component);
break;
case SharedDoorComponent.DoorState.Closing:
case SharedDoorComponent.DoorState.Opening:
_activeDoors.Add(message.Component);
break;
default:
throw new ArgumentOutOfRangeException();
}
}
/// <inheritdoc />
public override void Update(float frameTime)
{
for (var i = _activeDoors.Count - 1; i >= 0; i--)
{
var comp = _activeDoors[i];
if (comp.Deleted)
{
_activeDoors.RemoveAt(i);
}
comp.OnUpdate();
}
}
}
}

View File

@@ -31,7 +31,6 @@ namespace Content.Client
"MeleeChemicalInjector",
"Dice",
"Construction",
"Door",
"PoweredLight",
"Smes",
"LightBulb",

View File

@@ -1,5 +1,6 @@
using System.Threading.Tasks;
using Content.Server.GameObjects.Components.Doors;
using Content.Shared.GameObjects.Components.Doors;
using NUnit.Framework;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
@@ -29,6 +30,7 @@ namespace Content.IntegrationTests.Tests.Doors
name: AirlockDummy
id: AirlockDummy
components:
- type: Door
- type: Airlock
- type: Physics
shapes:
@@ -49,7 +51,7 @@ namespace Content.IntegrationTests.Tests.Doors
var entityManager = server.ResolveDependency<IEntityManager>();
IEntity airlock = null;
AirlockComponent airlockComponent = null;
ServerDoorComponent doorComponent = null;
server.Assert(() =>
{
@@ -57,33 +59,33 @@ namespace Content.IntegrationTests.Tests.Doors
airlock = entityManager.SpawnEntity("AirlockDummy", MapCoordinates.Nullspace);
Assert.True(airlock.TryGetComponent(out airlockComponent));
Assert.That(airlockComponent.State, Is.EqualTo(DoorState.Closed));
Assert.True(airlock.TryGetComponent(out doorComponent));
Assert.That(doorComponent.State, Is.EqualTo(SharedDoorComponent.DoorState.Closed));
});
await server.WaitIdleAsync();
server.Assert(() =>
{
airlockComponent.Open();
Assert.That(airlockComponent.State, Is.EqualTo(DoorState.Opening));
doorComponent.Open();
Assert.That(doorComponent.State, Is.EqualTo(SharedDoorComponent.DoorState.Opening));
});
await server.WaitIdleAsync();
await WaitUntil(server, () => airlockComponent.State == DoorState.Open);
await WaitUntil(server, () => doorComponent.State == SharedDoorComponent.DoorState.Open);
Assert.That(airlockComponent.State, Is.EqualTo(DoorState.Open));
Assert.That(doorComponent.State, Is.EqualTo(SharedDoorComponent.DoorState.Open));
server.Assert(() =>
{
airlockComponent.Close();
Assert.That(airlockComponent.State, Is.EqualTo(DoorState.Closing));
doorComponent.Close();
Assert.That(doorComponent.State, Is.EqualTo(SharedDoorComponent.DoorState.Closing));
});
await WaitUntil(server, () => airlockComponent.State == DoorState.Closed);
await WaitUntil(server, () => doorComponent.State == SharedDoorComponent.DoorState.Closed);
Assert.That(airlockComponent.State, Is.EqualTo(DoorState.Closed));
Assert.That(doorComponent.State, Is.EqualTo(SharedDoorComponent.DoorState.Closed));
server.Assert(() =>
{
@@ -112,7 +114,7 @@ namespace Content.IntegrationTests.Tests.Doors
IEntity physicsDummy = null;
IEntity airlock = null;
TestController controller = null;
AirlockComponent airlockComponent = null;
ServerDoorComponent doorComponent = null;
var physicsDummyStartingX = -1;
@@ -130,8 +132,8 @@ namespace Content.IntegrationTests.Tests.Doors
controller = physics.EnsureController<TestController>();
Assert.True(airlock.TryGetComponent(out airlockComponent));
Assert.That(airlockComponent.State, Is.EqualTo(DoorState.Closed));
Assert.True(airlock.TryGetComponent(out doorComponent));
Assert.That(doorComponent.State, Is.EqualTo(SharedDoorComponent.DoorState.Closed));
});
await server.WaitIdleAsync();
@@ -145,7 +147,7 @@ namespace Content.IntegrationTests.Tests.Doors
airlock.GetComponent<IPhysicsComponent>().WakeBody();
// Ensure that it is still closed
Assert.That(airlockComponent.State, Is.EqualTo(DoorState.Closed));
Assert.That(doorComponent.State, Is.EqualTo(SharedDoorComponent.DoorState.Closed));
await server.WaitRunTicks(10);
await server.WaitIdleAsync();

View File

@@ -1,104 +1,120 @@
using System;
using System.Threading.Tasks;
#nullable enable
using Content.Server.GameObjects.Components.Doors;
using Content.Server.GameObjects.Components.Interactable;
using Content.Server.Interfaces.GameObjects.Components.Doors;
using Content.Shared.GameObjects.Components.Doors;
using Content.Shared.GameObjects.Components.Interactable;
using Content.Shared.Interfaces;
using Content.Shared.Interfaces.GameObjects.Components;
using Robust.Shared.GameObjects;
using Content.Server.Atmos;
using Content.Server.GameObjects.EntitySystems;
using Robust.Shared.Localization;
namespace Content.Server.GameObjects.Components.Atmos
{
/// <summary>
/// Companion component to ServerDoorComponent that handles firelock-specific behavior -- primarily prying, and not being openable on open-hand click.
/// </summary>
[RegisterComponent]
[ComponentReference(typeof(ServerDoorComponent))]
public class FirelockComponent : ServerDoorComponent, IInteractUsing, ICollideBehavior
[ComponentReference(typeof(IDoorCheck))]
public class FirelockComponent : Component, IDoorCheck
{
public override string Name => "Firelock";
protected override TimeSpan CloseTimeOne => TimeSpan.FromSeconds(0.1f);
protected override TimeSpan CloseTimeTwo => TimeSpan.FromSeconds(0.6f);
protected override TimeSpan OpenTimeOne => TimeSpan.FromSeconds(0.1f);
protected override TimeSpan OpenTimeTwo => TimeSpan.FromSeconds(0.6f);
public void CollideWith(IEntity collidedWith)
{
// We do nothing.
}
protected override void Startup()
{
base.Startup();
if (Owner.TryGetComponent(out AirtightComponent airtightComponent))
{
airtightComponent.AirBlocked = false;
}
if (Owner.TryGetComponent(out IPhysicsComponent physics))
{
physics.CanCollide = false;
}
AutoClose = false;
Safety = false;
State = DoorState.Open;
SetAppearance(DoorVisualState.Open);
}
[ComponentDependency]
private readonly ServerDoorComponent? _doorComponent = null;
public bool EmergencyPressureStop()
{
var closed = State == DoorState.Open && Close();
if(closed)
Owner.GetComponent<AirtightComponent>().AirBlocked = true;
return closed;
if (_doorComponent != null && _doorComponent.State == SharedDoorComponent.DoorState.Open && _doorComponent.CanCloseGeneric())
{
_doorComponent.Close();
if (Owner.TryGetComponent(out AirtightComponent? airtight))
{
airtight.AirBlocked = true;
}
return true;
}
return false;
}
public override bool CanOpen()
bool IDoorCheck.OpenCheck()
{
return !IsHoldingFire() && !IsHoldingPressure() && base.CanOpen();
return !IsHoldingFire() && !IsHoldingPressure();
}
public override bool CanClose(IEntity user) => true;
public override bool CanOpen(IEntity user) => CanOpen();
bool IDoorCheck.DenyCheck() => false;
public override async Task<bool> InteractUsing(InteractUsingEventArgs eventArgs)
float? IDoorCheck.GetPryTime()
{
if (await base.InteractUsing(eventArgs))
if (IsHoldingFire() || IsHoldingPressure())
{
return 1.5f;
}
return null;
}
bool IDoorCheck.BlockActivate(ActivateEventArgs eventArgs) => true;
void IDoorCheck.OnStartPry(InteractUsingEventArgs eventArgs)
{
if (_doorComponent == null || _doorComponent.State != SharedDoorComponent.DoorState.Closed)
{
return;
}
if (IsHoldingPressure())
{
Owner.PopupMessage(eventArgs.User, Loc.GetString("A gush of air blows in your face... Maybe you should reconsider."));
}
else if (IsHoldingFire())
{
Owner.PopupMessage(eventArgs.User, Loc.GetString("A gush of warm air blows in your face... Maybe you should reconsider."));
}
}
public bool IsHoldingPressure(float threshold = 20)
{
var atmosphereSystem = EntitySystem.Get<AtmosphereSystem>();
if (!Owner.Transform.Coordinates.TryGetTileAtmosphere(out var tileAtmos))
return false;
if (!eventArgs.Using.TryGetComponent<ToolComponent>(out var tool))
var gridAtmosphere = atmosphereSystem.GetGridAtmosphere(Owner.Transform.GridID);
var minMoles = float.MaxValue;
var maxMoles = 0f;
foreach (var (_, adjacent) in gridAtmosphere.GetAdjacentTiles(tileAtmos.GridIndices))
{
// includeAirBlocked remains false, and therefore Air must be present
var moles = adjacent.Air!.TotalMoles;
if (moles < minMoles)
minMoles = moles;
if (moles > maxMoles)
maxMoles = moles;
}
return (maxMoles - minMoles) > threshold;
}
public bool IsHoldingFire()
{
var atmosphereSystem = EntitySystem.Get<AtmosphereSystem>();
if (!Owner.Transform.Coordinates.TryGetTileAtmosphere(out var tileAtmos))
return false;
if (tool.HasQuality(ToolQuality.Prying) && !IsWeldedShut)
{
var holdingPressure = IsHoldingPressure();
var holdingFire = IsHoldingFire();
if (tileAtmos.Hotspot.Valid)
return true;
if (State == DoorState.Closed)
{
if (holdingPressure)
Owner.PopupMessage(eventArgs.User, "A gush of air blows in your face... Maybe you should reconsider.");
}
var gridAtmosphere = atmosphereSystem.GetGridAtmosphere(Owner.Transform.GridID);
if (IsWeldedShut || !await tool.UseTool(eventArgs.User, Owner, holdingPressure || holdingFire ? 1.5f : 0.25f, ToolQuality.Prying)) return false;
if (State == DoorState.Closed)
foreach (var (_, adjacent) in gridAtmosphere.GetAdjacentTiles(tileAtmos.GridIndices))
{
Open();
}
else if (State == DoorState.Open)
{
Close();
}
if (adjacent.Hotspot.Valid)
return true;
}
return false;
}
}

View File

@@ -1,12 +1,10 @@
#nullable enable
using System;
using System.Threading;
using System.Threading.Tasks;
using Content.Server.GameObjects.Components.Interactable;
using Content.Server.GameObjects.Components.Power.ApcNetComponents;
using Content.Server.GameObjects.Components.VendingMachines;
using Content.Server.Interfaces.GameObjects.Components.Doors;
using Content.Shared.GameObjects.Components.Doors;
using Content.Shared.GameObjects.Components.Interactable;
using Content.Shared.Interfaces;
using Content.Shared.Interfaces.GameObjects.Components;
using Robust.Server.GameObjects;
@@ -19,13 +17,27 @@ using static Content.Shared.GameObjects.Components.SharedWiresComponent.WiresAct
namespace Content.Server.GameObjects.Components.Doors
{
/// <summary>
/// Companion component to ServerDoorComponent that handles airlock-specific behavior -- wires, requiring power to operate, bolts, and allowing automatic closing.
/// </summary>
[RegisterComponent]
[ComponentReference(typeof(IActivate))]
[ComponentReference(typeof(ServerDoorComponent))]
public class AirlockComponent : ServerDoorComponent, IWires
[ComponentReference(typeof(IDoorCheck))]
public class AirlockComponent : Component, IWires, IDoorCheck
{
public override string Name => "Airlock";
[ComponentDependency]
private readonly ServerDoorComponent? _doorComponent = null;
[ComponentDependency]
private readonly SharedAppearanceComponent? _appearanceComponent = null;
[ComponentDependency]
private readonly PowerReceiverComponent? _receiverComponent = null;
[ComponentDependency]
private readonly WiresComponent? _wiresComponent = null;
/// <summary>
/// Duration for which power will be disabled after pulsing either power wire.
/// </summary>
@@ -68,7 +80,8 @@ namespace Content.Server.GameObjects.Components.Doors
[ViewVariables(VVAccess.ReadWrite)]
private bool BoltLightsVisible
{
get => _boltLightsWirePulsed && BoltsDown && IsPowered() && State == DoorState.Closed;
get => _boltLightsWirePulsed && BoltsDown && IsPowered()
&& _doorComponent != null && _doorComponent.State == SharedDoorComponent.DoorState.Closed;
set
{
_boltLightsWirePulsed = value;
@@ -76,121 +89,24 @@ namespace Content.Server.GameObjects.Components.Doors
}
}
private const float AutoCloseDelayFast = 1;
// True => AutoCloseDelay; False => AutoCloseDelayFast
private static readonly TimeSpan AutoCloseDelayFast = TimeSpan.FromSeconds(1);
[ViewVariables(VVAccess.ReadWrite)]
private bool NormalCloseSpeed
{
get => CloseSpeed == AutoCloseDelay;
set => CloseSpeed = value ? AutoCloseDelay : AutoCloseDelayFast;
}
private bool _autoClose = true;
private void UpdateWiresStatus()
{
WiresComponent? wires;
var powerLight = new StatusLightData(Color.Yellow, StatusLightState.On, "POWR");
if (PowerWiresPulsed)
{
powerLight = new StatusLightData(Color.Yellow, StatusLightState.BlinkingFast, "POWR");
}
else if (Owner.TryGetComponent(out wires) &&
wires.IsWireCut(Wires.MainPower) &&
wires.IsWireCut(Wires.BackupPower))
{
powerLight = new StatusLightData(Color.Red, StatusLightState.On, "POWR");
}
[ViewVariables(VVAccess.ReadWrite)]
private bool _normalCloseSpeed = true;
var boltStatus =
new StatusLightData(Color.Red, BoltsDown ? StatusLightState.On : StatusLightState.Off, "BOLT");
var boltLightsStatus = new StatusLightData(Color.Lime,
_boltLightsWirePulsed ? StatusLightState.On : StatusLightState.Off, "BLTL");
var timingStatus =
new StatusLightData(Color.Orange, !AutoClose ? StatusLightState.Off :
!NormalCloseSpeed ? StatusLightState.BlinkingSlow :
StatusLightState.On,
"TIME");
var safetyStatus =
new StatusLightData(Color.Red, Safety ? StatusLightState.On : StatusLightState.Off, "SAFE");
if (!Owner.TryGetComponent(out wires))
{
return;
}
wires.SetStatus(AirlockWireStatus.PowerIndicator, powerLight);
wires.SetStatus(AirlockWireStatus.BoltIndicator, boltStatus);
wires.SetStatus(AirlockWireStatus.BoltLightIndicator, boltLightsStatus);
wires.SetStatus(AirlockWireStatus.AIControlIndicator, new StatusLightData(Color.Purple, StatusLightState.BlinkingSlow, "AICT"));
wires.SetStatus(AirlockWireStatus.TimingIndicator, timingStatus);
wires.SetStatus(AirlockWireStatus.SafetyIndicator, safetyStatus);
/*
_wires.SetStatus(6, powerLight);
_wires.SetStatus(7, powerLight);
_wires.SetStatus(8, powerLight);
_wires.SetStatus(9, powerLight);
_wires.SetStatus(10, powerLight);
_wires.SetStatus(11, powerLight);*/
}
private void UpdatePowerCutStatus()
{
if (!Owner.TryGetComponent(out PowerReceiverComponent? receiver))
{
return;
}
if (PowerWiresPulsed)
{
receiver.PowerDisabled = true;
return;
}
if (!Owner.TryGetComponent(out WiresComponent? wires))
{
return;
}
receiver.PowerDisabled =
wires.IsWireCut(Wires.MainPower) ||
wires.IsWireCut(Wires.BackupPower);
}
private void UpdateBoltLightStatus()
{
if (Owner.TryGetComponent(out AppearanceComponent? appearance))
{
appearance.SetData(DoorVisuals.BoltLights, BoltLightsVisible);
}
}
public override DoorState State
{
protected set
{
base.State = value;
// Only show the maintenance panel if the airlock is closed
if (Owner.TryGetComponent(out WiresComponent? wires))
{
wires.IsPanelVisible = value != DoorState.Open;
}
// If the door is closed, we should look if the bolt was locked while closing
UpdateBoltLightStatus();
}
}
[ViewVariables(VVAccess.ReadWrite)]
private bool _safety = true;
public override void Initialize()
{
base.Initialize();
if (Owner.TryGetComponent(out PowerReceiverComponent? receiver))
if (_receiverComponent != null && _appearanceComponent != null)
{
if (Owner.TryGetComponent(out AppearanceComponent? appearance))
{
appearance.SetData(DoorVisuals.Powered, receiver.Powered);
}
_appearanceComponent.SetData(DoorVisuals.Powered, _receiverComponent.Powered);
}
}
@@ -205,33 +121,172 @@ namespace Content.Server.GameObjects.Components.Doors
}
}
void IDoorCheck.OnStateChange(SharedDoorComponent.DoorState doorState)
{
// Only show the maintenance panel if the airlock is closed
if (_wiresComponent != null)
{
_wiresComponent.IsPanelVisible = doorState != SharedDoorComponent.DoorState.Open;
}
// If the door is closed, we should look if the bolt was locked while closing
UpdateBoltLightStatus();
}
bool IDoorCheck.OpenCheck() => CanChangeState();
bool IDoorCheck.CloseCheck() => CanChangeState();
bool IDoorCheck.DenyCheck() => CanChangeState();
bool IDoorCheck.SafetyCheck() => _safety;
bool IDoorCheck.AutoCloseCheck() => _autoClose;
TimeSpan? IDoorCheck.GetCloseSpeed()
{
if (_normalCloseSpeed)
{
return null;
}
return AutoCloseDelayFast;
}
bool IDoorCheck.BlockActivate(ActivateEventArgs eventArgs)
{
if (_wiresComponent != null && _wiresComponent.IsPanelOpen &&
eventArgs.User.TryGetComponent(out IActorComponent? actor))
{
_wiresComponent.OpenInterface(actor.playerSession);
return true;
}
return false;
}
bool IDoorCheck.CanPryCheck(InteractUsingEventArgs eventArgs)
{
if (IsBolted())
{
Owner.PopupMessage(eventArgs.User, Loc.GetString("The airlock's bolts prevent it from being forced!"));
return false;
}
if (IsPowered())
{
Owner.PopupMessage(eventArgs.User, Loc.GetString("The powered motors block your efforts!"));
return false;
}
return true;
}
private bool CanChangeState()
{
return IsPowered() && !IsBolted();
}
private bool IsBolted()
{
return _boltsDown;
}
private bool IsPowered()
{
return _receiverComponent == null || _receiverComponent.Powered;
}
private void UpdateBoltLightStatus()
{
if (_appearanceComponent != null)
{
_appearanceComponent.SetData(DoorVisuals.BoltLights, BoltLightsVisible);
}
}
private void UpdateWiresStatus()
{
if (_doorComponent == null)
{
return;
}
var powerLight = new StatusLightData(Color.Yellow, StatusLightState.On, "POWR");
if (PowerWiresPulsed)
{
powerLight = new StatusLightData(Color.Yellow, StatusLightState.BlinkingFast, "POWR");
}
else if (_wiresComponent != null &&
_wiresComponent.IsWireCut(Wires.MainPower) &&
_wiresComponent.IsWireCut(Wires.BackupPower))
{
powerLight = new StatusLightData(Color.Red, StatusLightState.On, "POWR");
}
var boltStatus =
new StatusLightData(Color.Red, BoltsDown ? StatusLightState.On : StatusLightState.Off, "BOLT");
var boltLightsStatus = new StatusLightData(Color.Lime,
_boltLightsWirePulsed ? StatusLightState.On : StatusLightState.Off, "BLTL");
var timingStatus =
new StatusLightData(Color.Orange, !_autoClose ? StatusLightState.Off :
!_normalCloseSpeed ? StatusLightState.BlinkingSlow :
StatusLightState.On,
"TIME");
var safetyStatus =
new StatusLightData(Color.Red, _safety ? StatusLightState.On : StatusLightState.Off, "SAFE");
if (_wiresComponent == null)
{
return;
}
_wiresComponent.SetStatus(AirlockWireStatus.PowerIndicator, powerLight);
_wiresComponent.SetStatus(AirlockWireStatus.BoltIndicator, boltStatus);
_wiresComponent.SetStatus(AirlockWireStatus.BoltLightIndicator, boltLightsStatus);
_wiresComponent.SetStatus(AirlockWireStatus.AIControlIndicator, new StatusLightData(Color.Purple, StatusLightState.BlinkingSlow, "AICT"));
_wiresComponent.SetStatus(AirlockWireStatus.TimingIndicator, timingStatus);
_wiresComponent.SetStatus(AirlockWireStatus.SafetyIndicator, safetyStatus);
/*
_wires.SetStatus(6, powerLight);
_wires.SetStatus(7, powerLight);
_wires.SetStatus(8, powerLight);
_wires.SetStatus(9, powerLight);
_wires.SetStatus(10, powerLight);
_wires.SetStatus(11, powerLight);*/
}
private void UpdatePowerCutStatus()
{
if (_receiverComponent == null)
{
return;
}
if (PowerWiresPulsed)
{
_receiverComponent.PowerDisabled = true;
return;
}
if (_wiresComponent == null)
{
return;
}
_receiverComponent.PowerDisabled =
_wiresComponent.IsWireCut(Wires.MainPower) ||
_wiresComponent.IsWireCut(Wires.BackupPower);
}
private void PowerDeviceOnOnPowerStateChanged(PowerChangedMessage e)
{
if (Owner.TryGetComponent(out AppearanceComponent? appearance))
if (_appearanceComponent != null)
{
appearance.SetData(DoorVisuals.Powered, e.Powered);
_appearanceComponent.SetData(DoorVisuals.Powered, e.Powered);
}
// BoltLights also got out
UpdateBoltLightStatus();
}
protected override void ActivateImpl(ActivateEventArgs args)
{
if (Owner.TryGetComponent(out WiresComponent? wires) &&
wires.IsPanelOpen)
{
if (args.User.TryGetComponent(out IActorComponent? actor))
{
wires.OpenInterface(actor.playerSession);
}
}
else
{
base.ActivateImpl(args);
}
}
private enum Wires
{
/// <summary>
@@ -296,6 +351,11 @@ namespace Content.Server.GameObjects.Components.Doors
public void WiresUpdate(WiresUpdateEventArgs args)
{
if(_doorComponent == null)
{
return;
}
if (args.Action == Pulse)
{
switch (args.Identifier)
@@ -328,10 +388,11 @@ namespace Content.Server.GameObjects.Components.Doors
BoltLightsVisible = !_boltLightsWirePulsed;
break;
case Wires.Timing:
NormalCloseSpeed = !NormalCloseSpeed;
_normalCloseSpeed = !_normalCloseSpeed;
_doorComponent.RefreshAutoClose();
break;
case Wires.Safety:
Safety = !Safety;
_safety = !_safety;
break;
}
}
@@ -350,10 +411,11 @@ namespace Content.Server.GameObjects.Components.Doors
BoltLightsVisible = true;
break;
case Wires.Timing:
AutoClose = true;
_autoClose = true;
_doorComponent.RefreshAutoClose();
break;
case Wires.Safety:
Safety = true;
_safety = true;
break;
}
}
@@ -369,10 +431,11 @@ namespace Content.Server.GameObjects.Components.Doors
BoltLightsVisible = false;
break;
case Wires.Timing:
AutoClose = false;
_autoClose = false;
_doorComponent.RefreshAutoClose();
break;
case Wires.Safety:
Safety = false;
_safety = false;
break;
}
}
@@ -381,87 +444,6 @@ namespace Content.Server.GameObjects.Components.Doors
UpdatePowerCutStatus();
}
public override bool CanOpen()
{
return base.CanOpen() && IsPowered() && !IsBolted();
}
public override bool CanClose()
{
return IsPowered() && !IsBolted();
}
public override void Deny()
{
if (!IsPowered() || IsBolted())
{
return;
}
base.Deny();
}
private bool IsBolted()
{
return _boltsDown;
}
private bool IsPowered()
{
return !Owner.TryGetComponent(out PowerReceiverComponent? receiver)
|| receiver.Powered;
}
public override async Task<bool> InteractUsing(InteractUsingEventArgs eventArgs)
{
if (await base.InteractUsing(eventArgs))
return true;
if (!eventArgs.Using.TryGetComponent<ToolComponent>(out var tool))
return false;
if (tool.HasQuality(ToolQuality.Cutting)
|| tool.HasQuality(ToolQuality.Multitool))
{
if (Owner.TryGetComponent(out WiresComponent? wires)
&& wires.IsPanelOpen)
{
if (eventArgs.User.TryGetComponent(out IActorComponent? actor))
{
wires.OpenInterface(actor.playerSession);
return true;
}
}
}
bool AirlockCheck()
{
if (IsBolted())
{
Owner.PopupMessage(eventArgs.User,
Loc.GetString("The airlock's bolts prevent it from being forced!"));
return false;
}
if (IsPowered())
{
Owner.PopupMessage(eventArgs.User, Loc.GetString("The powered motors block your efforts!"));
return false;
}
return true;
}
if (!await tool.UseTool(eventArgs.User, Owner, 0.2f, ToolQuality.Prying, AirlockCheck)) return false;
if (State == DoorState.Closed)
Open();
else if (State == DoorState.Open)
Close();
return true;
}
public void SetBoltsWithAudio(bool newBolts)
{
if (newBolts == BoltsDown)

View File

@@ -1,8 +1,7 @@
#nullable enable
#nullable enable
using System;
using System.Linq;
using System.Threading;
using Content.Server.Atmos;
using System.Threading.Tasks;
using Content.Server.GameObjects.Components.Access;
using Content.Server.GameObjects.Components.Atmos;
@@ -10,6 +9,7 @@ using Content.Server.GameObjects.Components.GUI;
using Content.Server.GameObjects.Components.Interactable;
using Content.Server.GameObjects.Components.Mobs;
using Content.Server.GameObjects.EntitySystems;
using Content.Server.Interfaces.GameObjects.Components.Doors;
using Content.Shared.Damage;
using Content.Shared.GameObjects.Components.Body;
using Content.Shared.GameObjects.Components.Damage;
@@ -20,7 +20,10 @@ using Content.Shared.Interfaces.GameObjects.Components;
using Robust.Server.GameObjects;
using Robust.Shared.Audio;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Maths;
using Robust.Shared.Physics;
using Robust.Shared.Serialization;
using Robust.Shared.ViewVariables;
using Timer = Robust.Shared.Timers.Timer;
@@ -29,56 +32,87 @@ namespace Content.Server.GameObjects.Components.Doors
{
[RegisterComponent]
[ComponentReference(typeof(IActivate))]
public class ServerDoorComponent : Component, IActivate, ICollideBehavior, IInteractUsing
[ComponentReference(typeof(SharedDoorComponent))]
public class ServerDoorComponent : SharedDoorComponent, IActivate, ICollideBehavior, IInteractUsing, IMapInit
{
public override string Name => "Door";
[ComponentDependency]
private readonly IDoorCheck? _doorCheck = null;
[ViewVariables]
private DoorState _state = DoorState.Closed;
public virtual DoorState State
public override DoorState State
{
get => _state;
get => base.State;
protected set
{
if (_state == value)
if (State == value)
{
return;
}
_state = value;
base.State = value;
Owner.EntityManager.EventBus.RaiseEvent(EventSource.Local, new DoorStateMessage(this, State));
StateChangeStartTime = State switch
{
DoorState.Open or DoorState.Closed => null,
DoorState.Opening or DoorState.Closing => GameTiming.CurTime,
_ => throw new ArgumentOutOfRangeException(),
};
if (_doorCheck != null)
{
_doorCheck.OnStateChange(State);
RefreshAutoClose();
}
Dirty();
}
}
/// <summary>
/// The amount of time the door has been open. Used to automatically close the door if it autocloses.
/// </summary>
[ViewVariables]
protected float OpenTimeCounter;
private float _openTimeCounter;
[ViewVariables(VVAccess.ReadWrite)]
protected bool AutoClose = true;
protected const float AutoCloseDelay = 5;
[ViewVariables(VVAccess.ReadWrite)]
protected float CloseSpeed = AutoCloseDelay;
private CancellationTokenSource? _cancellationTokenSource;
private static readonly TimeSpan AutoCloseDelay = TimeSpan.FromSeconds(5);
protected virtual TimeSpan CloseTimeOne => TimeSpan.FromSeconds(0.3f);
protected virtual TimeSpan CloseTimeTwo => TimeSpan.FromSeconds(0.9f);
protected virtual TimeSpan OpenTimeOne => TimeSpan.FromSeconds(0.3f);
protected virtual TimeSpan OpenTimeTwo => TimeSpan.FromSeconds(0.9f);
protected virtual TimeSpan DenyTime => TimeSpan.FromSeconds(0.45f);
private CancellationTokenSource? _stateChangeCancelTokenSource;
private CancellationTokenSource? _autoCloseCancelTokenSource;
private const int DoorCrushDamage = 15;
private const float DoorStunTime = 5f;
[ViewVariables(VVAccess.ReadWrite)]
protected bool Safety = true;
/// <summary>
/// Whether the door will ever crush.
/// </summary>
[ViewVariables(VVAccess.ReadOnly)]
private bool _inhibitCrush;
/// <summary>
/// Whether the door blocks light.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)] private bool _occludes;
public bool Occludes => _occludes;
/// <summary>
/// Whether the door will open when it is bumped into.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)] private bool _bumpOpen;
public bool BumpOpen => _bumpOpen;
/// <summary>
/// Whether the door starts open when it's first loaded from prototype. A door won't start open if its prototype is also welded shut.
/// Handled in Startup().
/// </summary>
private bool _startOpen;
/// <summary>
/// Whether the airlock is welded shut. Can be set by the prototype, although this will fail if the door isn't weldable.
/// When set by prototype, handled in Startup().
/// </summary>
private bool _isWeldedShut;
/// <summary>
/// Whether the airlock is welded shut.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
public bool IsWeldedShut
{
@@ -94,37 +128,76 @@ namespace Content.Server.GameObjects.Components.Doors
SetAppearance(_isWeldedShut ? DoorVisualState.Welded : DoorVisualState.Closed);
}
}
private bool _isWeldedShut;
private bool _canWeldShut = true;
/// <summary>
/// Whether the door can ever be welded shut.
/// </summary>
private bool _weldable;
/// <summary>
/// Whether the door can currently be welded.
/// </summary>
private bool CanWeldShut => _weldable && State == DoorState.Closed;
/// <summary>
/// Whether something is currently using a welder on this so DoAfter isn't spammed.
/// </summary>
private bool _beingWelded;
[ViewVariables(VVAccess.ReadWrite)]
private bool _canCrush = true;
private bool _beingWelded = false;
public override void ExposeData(ObjectSerializer serializer)
{
base.ExposeData(serializer);
serializer.DataField(ref _occludes, "occludes", true);
serializer.DataField(ref _bumpOpen, "bumpOpen", true);
serializer.DataField(ref _isWeldedShut, "welded", false);
serializer.DataField(ref _canCrush, "canCrush", true);
serializer.DataField(ref _startOpen, "startOpen", false);
serializer.DataField(ref _weldable, "weldable", true);
serializer.DataField(ref _bumpOpen, "bumpOpen", true);
serializer.DataField(ref _occludes, "occludes", true);
serializer.DataField(ref _inhibitCrush, "inhibitCrush", false);
}
protected override void Startup()
{
base.Startup();
if (IsWeldedShut)
{
if (!CanWeldShut)
{
Logger.Warning("{0} prototype loaded with incompatible flags: 'welded' is true, but door cannot be welded.", Owner.Name);
return;
}
SetAppearance(DoorVisualState.Welded);
}
}
public override void OnRemove()
{
_cancellationTokenSource?.Cancel();
_stateChangeCancelTokenSource?.Cancel();
_autoCloseCancelTokenSource?.Cancel();
base.OnRemove();
}
protected virtual void ActivateImpl(ActivateEventArgs eventArgs)
void IMapInit.MapInit()
{
if (_startOpen)
{
if (IsWeldedShut)
{
Logger.Warning("{0} prototype loaded with incompatible flags: 'welded' and 'startOpen' are both true.", Owner.Name);
return;
}
QuickOpen();
}
}
void IActivate.Activate(ActivateEventArgs eventArgs)
{
if (_doorCheck != null && _doorCheck.BlockActivate(eventArgs))
{
return;
}
if (State == DoorState.Open)
{
TryClose(eventArgs.User);
@@ -135,11 +208,6 @@ namespace Content.Server.GameObjects.Components.Doors
}
}
void IActivate.Activate(ActivateEventArgs eventArgs)
{
ActivateImpl(eventArgs);
}
void ICollideBehavior.CollideWith(IEntity entity)
{
if (State != DoorState.Closed)
@@ -173,57 +241,11 @@ namespace Content.Server.GameObjects.Components.Doors
}
}
protected void SetAppearance(DoorVisualState state)
{
if (Owner.TryGetComponent(out AppearanceComponent? appearance))
{
appearance.SetData(DoorVisuals.VisualState, state);
}
}
public virtual bool CanOpen()
{
return !_isWeldedShut;
}
public virtual bool CanOpen(IEntity user)
{
if (!CanOpen()) return false;
if (!Owner.TryGetComponent<AccessReader>(out var accessReader))
{
return true;
}
var doorSystem = EntitySystem.Get<DoorSystem>();
var isAirlockExternal = HasAccessType("External");
return doorSystem.AccessType switch
{
DoorSystem.AccessTypes.AllowAll => true,
DoorSystem.AccessTypes.AllowAllIdExternal => isAirlockExternal ? accessReader.IsAllowed(user) : true,
DoorSystem.AccessTypes.AllowAllNoExternal => !isAirlockExternal,
_ => accessReader.IsAllowed(user)
};
}
/// <summary>
/// Returns whether a door has a certain access type. For example, maintenance doors will have access type
/// "Maintenance" in their AccessReader.
/// </summary>
private bool HasAccessType(string accesType)
{
if(Owner.TryGetComponent<AccessReader>(out var accessReader))
{
return accessReader.AccessLists.Any(list => list.Contains(accesType));
}
return true;
}
#region Opening
public void TryOpen(IEntity user)
{
if (CanOpen(user))
if (CanOpenByEntity(user))
{
Open();
@@ -239,64 +261,113 @@ namespace Content.Server.GameObjects.Components.Doors
}
}
public void Open()
public bool CanOpenByEntity(IEntity user)
{
if (State != DoorState.Closed)
if(!CanOpenGeneric())
{
return;
return false;
}
_canWeldShut = false;
if (!Owner.TryGetComponent(out AccessReader? access))
{
return true;
}
var doorSystem = EntitySystem.Get<ServerDoorSystem>();
var isAirlockExternal = HasAccessType("External");
return doorSystem.AccessType switch
{
ServerDoorSystem.AccessTypes.AllowAll => true,
ServerDoorSystem.AccessTypes.AllowAllIdExternal => isAirlockExternal || access.IsAllowed(user),
ServerDoorSystem.AccessTypes.AllowAllNoExternal => !isAirlockExternal,
_ => access.IsAllowed(user)
};
}
/// <summary>
/// Returns whether a door has a certain access type. For example, maintenance doors will have access type
/// "Maintenance" in their AccessReader.
/// </summary>
private bool HasAccessType(string accessType)
{
if (Owner.TryGetComponent(out AccessReader? access))
{
return access.AccessLists.Any(list => list.Contains(accessType));
}
return true;
}
/// <summary>
/// Checks if we can open at all, for anyone or anything. Will return false if inhibited by an IDoorCheck component.
/// </summary>
/// <returns>Boolean describing whether this door can open.</returns>
public bool CanOpenGeneric()
{
// note the welded check -- CanCloseGeneric does not have this
if (IsWeldedShut)
{
return false;
}
if(_doorCheck != null)
{
return _doorCheck.OpenCheck();
}
return true;
}
/// <summary>
/// Opens the door. Does not check if this is possible.
/// </summary>
public void Open()
{
State = DoorState.Opening;
SetAppearance(DoorVisualState.Opening);
if (_occludes && Owner.TryGetComponent(out OccluderComponent? occluder))
if (Occludes && Owner.TryGetComponent(out OccluderComponent? occluder))
{
occluder.Enabled = false;
}
_cancellationTokenSource?.Cancel();
_cancellationTokenSource = new();
_stateChangeCancelTokenSource?.Cancel();
_stateChangeCancelTokenSource = new();
Owner.SpawnTimer(OpenTimeOne, async () =>
{
OnPartialOpen();
await Timer.Delay(OpenTimeTwo, _stateChangeCancelTokenSource.Token);
State = DoorState.Open;
}, _stateChangeCancelTokenSource.Token);
}
protected override void OnPartialOpen()
{
if (Owner.TryGetComponent(out AirtightComponent? airtight))
{
airtight.AirBlocked = false;
}
if (Owner.TryGetComponent(out IPhysicsComponent? physics))
{
physics.CanCollide = false;
}
await Timer.Delay(OpenTimeTwo, _cancellationTokenSource.Token);
State = DoorState.Open;
SetAppearance(DoorVisualState.Open);
}, _cancellationTokenSource.Token);
base.OnPartialOpen();
Owner.EntityManager.EventBus.RaiseEvent(EventSource.Local, new AccessReaderChangeMessage(Owner, false));
}
public virtual bool CanClose()
private void QuickOpen()
{
return true;
if (Occludes && Owner.TryGetComponent(out OccluderComponent? occluder))
{
occluder.Enabled = false;
}
OnPartialOpen();
State = DoorState.Open;
}
public virtual bool CanClose(IEntity user)
{
if (!CanClose()) return false;
if (!Owner.TryGetComponent(out AccessReader? accessReader))
{
return true;
}
#endregion
return accessReader.IsAllowed(user);
}
#region Closing
public void TryClose(IEntity user)
{
if (!CanClose(user))
if (!CanCloseByEntity(user))
{
Deny();
return;
@@ -305,220 +376,275 @@ namespace Content.Server.GameObjects.Components.Doors
Close();
}
private void CheckCrush()
public bool CanCloseByEntity(IEntity user)
{
if (!Owner.TryGetComponent(out IPhysicsComponent? body))
return;
// Crush
foreach (var e in body.GetCollidingEntities(Vector2.Zero, false))
if (!CanCloseGeneric())
{
if (!e.TryGetComponent(out StunnableComponent? stun)
|| !e.TryGetComponent(out IDamageableComponent? damage)
|| !e.TryGetComponent(out IPhysicsComponent? otherBody))
continue;
var percentage = otherBody.WorldAABB.IntersectPercentage(body.WorldAABB);
if (percentage < 0.1f)
continue;
damage.ChangeDamage(DamageType.Blunt, DoorCrushDamage, false, Owner);
stun.Paralyze(DoorStunTime);
// If we hit someone, open up after stun (opens right when stun ends)
Owner.SpawnTimer(TimeSpan.FromSeconds(DoorStunTime) - OpenTimeOne - OpenTimeTwo, Open);
break;
}
}
public bool IsHoldingPressure(float threshold = 20)
{
var atmosphereSystem = EntitySystem.Get<AtmosphereSystem>();
if (!Owner.Transform.Coordinates.TryGetTileAtmosphere(out var tileAtmos))
return false;
var gridAtmosphere = atmosphereSystem.GetGridAtmosphere(Owner.Transform.GridID);
var minMoles = float.MaxValue;
var maxMoles = 0f;
foreach (var (_, adjacent) in gridAtmosphere.GetAdjacentTiles(tileAtmos.GridIndices))
{
// includeAirBlocked remains false, and therefore Air must be present
var moles = adjacent.Air!.TotalMoles;
if (moles < minMoles)
minMoles = moles;
if (moles > maxMoles)
maxMoles = moles;
}
return (maxMoles - minMoles) > threshold;
}
public bool IsHoldingFire()
if (!Owner.TryGetComponent(out AccessReader? access))
{
var atmosphereSystem = EntitySystem.Get<AtmosphereSystem>();
if (!Owner.Transform.Coordinates.TryGetTileAtmosphere(out var tileAtmos))
return false;
if (tileAtmos.Hotspot.Valid)
return true;
var gridAtmosphere = atmosphereSystem.GetGridAtmosphere(Owner.Transform.GridID);
foreach (var (_, adjacent) in gridAtmosphere.GetAdjacentTiles(tileAtmos.GridIndices))
{
if (adjacent.Hotspot.Valid)
return true;
}
return access.IsAllowed(user);
}
/// <summary>
/// Checks if we can close at all, for anyone or anything. Will return false if inhibited by an IDoorCheck component or if we are colliding with somebody while our Safety is on.
/// </summary>
/// <returns>Boolean describing whether this door can close.</returns>
public bool CanCloseGeneric()
{
if (_doorCheck != null && !_doorCheck.CloseCheck())
{
return false;
}
public bool Close()
{
bool shouldCheckCrush = false;
if (Owner.TryGetComponent(out IPhysicsComponent? physics))
physics.CanCollide = true;
return !IsSafetyColliding();
}
if (_canCrush && physics != null &&
physics.IsColliding(Vector2.Zero, false))
private bool SafetyCheck()
{
if (Safety)
return (_doorCheck != null && _doorCheck.SafetyCheck()) || _inhibitCrush;
}
/// <summary>
/// Checks if we care about safety, and if so, if something is colliding with it; ignores the CanCollide of the door's PhysicsComponent.
/// </summary>
/// <returns>True if something is colliding with us and we shouldn't crush things, false otherwise.</returns>
private bool IsSafetyColliding()
{
physics.CanCollide = false;
var safety = SafetyCheck();
if (safety && PhysicsComponent != null)
{
var physics = IoCManager.Resolve<IPhysicsManager>();
foreach(var e in physics.GetCollidingEntities(Owner.Transform.MapID, PhysicsComponent.WorldAABB))
{
if (e.CanCollide &&
((PhysicsComponent.CollisionMask & e.CollisionLayer) != 0x0 ||
(PhysicsComponent.CollisionLayer & e.CollisionMask) != 0x0))
{
return true;
}
}
}
return false;
}
// check if we crush someone while closing
shouldCheckCrush = true;
}
/// <summary>
/// Closes the door. Does not check if this is possible.
/// </summary>
public void Close()
{
State = DoorState.Closing;
OpenTimeCounter = 0;
SetAppearance(DoorVisualState.Closing);
if (_occludes && Owner.TryGetComponent(out OccluderComponent? occluder))
_openTimeCounter = 0;
// no more autoclose; we ARE closed
_autoCloseCancelTokenSource?.Cancel();
_stateChangeCancelTokenSource?.Cancel();
_stateChangeCancelTokenSource = new();
Owner.SpawnTimer(CloseTimeOne, async () =>
{
// if somebody walked into the door as it was closing, and we don't crush things
if (IsSafetyColliding())
{
Open();
return;
}
OnPartialClose();
await Timer.Delay(CloseTimeTwo, _stateChangeCancelTokenSource.Token);
if (Occludes && Owner.TryGetComponent(out OccluderComponent? occluder))
{
occluder.Enabled = true;
}
_cancellationTokenSource?.Cancel();
_cancellationTokenSource = new();
Owner.SpawnTimer(CloseTimeOne, async () =>
{
if (shouldCheckCrush && _canCrush)
{
CheckCrush();
State = DoorState.Closed;
}, _stateChangeCancelTokenSource.Token);
}
if (Owner.TryGetComponent(out AirtightComponent? airtight))
protected override void OnPartialClose()
{
base.OnPartialClose();
// if safety is off, crushes people inside of the door, temporarily turning off collisions with them while doing so.
var becomeairtight = SafetyCheck() || !TryCrush();
if (becomeairtight && Owner.TryGetComponent(out AirtightComponent? airtight))
{
airtight.AirBlocked = true;
}
if (Owner.TryGetComponent(out IPhysicsComponent? body))
{
body.CanCollide = true;
Owner.EntityManager.EventBus.RaiseEvent(EventSource.Local, new AccessReaderChangeMessage(Owner, true));
}
await Timer.Delay(CloseTimeTwo, _cancellationTokenSource.Token);
/// <summary>
/// Crushes everyone colliding with us by more than 10%.
/// </summary>
/// <returns>True if we crushed somebody, false if we did not.</returns>
private bool TryCrush()
{
if (PhysicsComponent == null)
{
return false;
}
_canWeldShut = true;
State = DoorState.Closed;
SetAppearance(DoorVisualState.Closed);
}, _cancellationTokenSource.Token);
Owner.EntityManager.EventBus.RaiseEvent(EventSource.Local, new AccessReaderChangeMessage(Owner, true));
var collidingentities = PhysicsComponent.GetCollidingEntities(Vector2.Zero, false);
if (!collidingentities.Any())
{
return false;
}
var doorAABB = PhysicsComponent.WorldAABB;
var hitsomebody = false;
// Crush
foreach (var e in collidingentities)
{
if (!e.TryGetComponent(out StunnableComponent? stun)
|| !e.TryGetComponent(out IDamageableComponent? damage)
|| !e.TryGetComponent(out IPhysicsComponent? otherBody))
{
continue;
}
var percentage = otherBody.WorldAABB.IntersectPercentage(doorAABB);
if (percentage < 0.1f)
continue;
hitsomebody = true;
CurrentlyCrushing.Add(e.Uid);
damage.ChangeDamage(DamageType.Blunt, DoorCrushDamage, false, Owner);
stun.Paralyze(DoorStunTime);
}
// If we hit someone, open up after stun (opens right when stun ends)
if (hitsomebody)
{
Owner.SpawnTimer(TimeSpan.FromSeconds(DoorStunTime) - OpenTimeOne - OpenTimeTwo, Open);
return true;
}
public virtual void Deny()
return false;
}
#endregion
public void Deny()
{
if (State == DoorState.Open || _isWeldedShut)
if (_doorCheck != null && !_doorCheck.DenyCheck())
{
return;
}
if (State == DoorState.Open || IsWeldedShut)
return;
_cancellationTokenSource?.Cancel();
_cancellationTokenSource = new();
_stateChangeCancelTokenSource?.Cancel();
_stateChangeCancelTokenSource = new();
SetAppearance(DoorVisualState.Deny);
Owner.SpawnTimer(DenyTime, () =>
{
SetAppearance(DoorVisualState.Closed);
}, _cancellationTokenSource.Token);
}, _stateChangeCancelTokenSource.Token);
}
public virtual void OnUpdate(float frameTime)
/// <summary>
/// Stops the current auto-close timer if there is one. Starts a new one if this is appropriate (i.e. entity has an IDoorCheck component that allows auto-closing).
/// </summary>
public void RefreshAutoClose()
{
if (State != DoorState.Open)
_autoCloseCancelTokenSource?.Cancel();
if (State != DoorState.Open || _doorCheck == null || !_doorCheck.AutoCloseCheck())
{
return;
}
_autoCloseCancelTokenSource = new();
if (AutoClose)
var realCloseTime = _doorCheck.GetCloseSpeed() ?? AutoCloseDelay;
Owner.SpawnTimer(realCloseTime, async () =>
{
OpenTimeCounter += frameTime;
if (CanCloseGeneric())
{
// Close() cancels _autoCloseCancellationTokenSource, so we're fine.
Close();
}
}, _autoCloseCancelTokenSource.Token);
}
if (OpenTimeCounter > CloseSpeed)
async Task<bool> IInteractUsing.InteractUsing(InteractUsingEventArgs eventArgs)
{
if (!CanClose() || !Close())
if(!eventArgs.Using.TryGetComponent(out ToolComponent? tool))
{
// Try again in 2 seconds if it's jammed or something.
OpenTimeCounter -= 2;
}
}
}
public enum DoorState
{
Closed,
Open,
Closing,
Opening,
}
public virtual async Task<bool> InteractUsing(InteractUsingEventArgs eventArgs)
{
if (!_canWeldShut)
{
_beingWelded = false;
return false;
}
if (!eventArgs.Using.TryGetComponent(out WelderComponent? tool) || !tool.WelderLit)
// for prying doors
if (tool.HasQuality(ToolQuality.Prying) && !IsWeldedShut)
{
_beingWelded = false;
return false;
var successfulPry = false;
if (_doorCheck != null)
{
_doorCheck.OnStartPry(eventArgs);
successfulPry = await tool.UseTool(eventArgs.User, Owner,
_doorCheck.GetPryTime() ?? 0.5f, ToolQuality.Prying, () => _doorCheck.CanPryCheck(eventArgs));
}
else
{
successfulPry = await tool.UseTool(eventArgs.User, Owner, 0.5f, ToolQuality.Prying);
}
if (_beingWelded)
return false;
_beingWelded = true;
if (!await tool.UseTool(eventArgs.User, Owner, 3f, ToolQuality.Welding, 3f, () => _canWeldShut))
if (successfulPry && !IsWeldedShut)
{
_beingWelded = false;
return false;
if (State == DoorState.Closed)
{
Open();
}
else if (State == DoorState.Open)
{
Close();
}
_beingWelded = false;
IsWeldedShut ^= true;
return true;
}
}
public sealed class DoorStateMessage : EntitySystemMessage
// for welding doors
if (CanWeldShut && tool.Owner.TryGetComponent(out WelderComponent? welder) && welder.WelderLit)
{
public ServerDoorComponent Component { get; }
public ServerDoorComponent.DoorState State { get; }
if(!_beingWelded)
{
_beingWelded = true;
if(await welder.UseTool(eventArgs.User, Owner, 3f, ToolQuality.Welding, 3f, () => CanWeldShut))
{
_beingWelded = false;
IsWeldedShut = !IsWeldedShut;
return true;
}
_beingWelded = false;
}
}
else
{
_beingWelded = false;
}
return false;
}
public DoorStateMessage(ServerDoorComponent component, ServerDoorComponent.DoorState state)
public override ComponentState GetComponentState()
{
Component = component;
State = state;
return new DoorComponentState(State, StateChangeStartTime, CurrentlyCrushing, GameTiming.CurTime);
}
}
}

View File

@@ -1,78 +0,0 @@
using System;
using System.Collections.Generic;
using Content.Server.GameObjects.Components.Doors;
using JetBrains.Annotations;
using Robust.Shared.GameObjects;
namespace Content.Server.GameObjects.EntitySystems
{
[UsedImplicitly]
class DoorSystem : EntitySystem
{
/// <summary>
/// Determines the base access behavior of all doors on the station.
/// </summary>
public AccessTypes AccessType { get; set; }
/// <summary>
/// How door access should be handled.
/// </summary>
public enum AccessTypes
{
/// <summary> ID based door access. </summary>
Id,
/// <summary>
/// Allows everyone to open doors, except external which airlocks are still handled with ID's
/// </summary>
AllowAllIdExternal,
/// <summary>
/// Allows everyone to open doors, except external airlocks which are never allowed, even if the user has
/// ID access.
/// </summary>
AllowAllNoExternal,
/// <summary> Allows everyone to open all doors. </summary>
AllowAll
}
private readonly List<ServerDoorComponent> _activeDoors = new();
public override void Initialize()
{
base.Initialize();
AccessType = AccessTypes.Id;
SubscribeLocalEvent<DoorStateMessage>(HandleDoorState);
}
private void HandleDoorState(DoorStateMessage message)
{
switch (message.State)
{
case ServerDoorComponent.DoorState.Closed:
_activeDoors.Remove(message.Component);
break;
case ServerDoorComponent.DoorState.Open:
_activeDoors.Add(message.Component);
break;
case ServerDoorComponent.DoorState.Closing:
case ServerDoorComponent.DoorState.Opening:
break;
default:
throw new ArgumentOutOfRangeException();
}
}
/// <inheritdoc />
public override void Update(float frameTime)
{
for (var i = _activeDoors.Count - 1; i >= 0; i--)
{
var comp = _activeDoors[i];
if (comp.Deleted)
_activeDoors.RemoveAt(i);
comp.OnUpdate(frameTime);
}
}
}
}

View File

@@ -0,0 +1,44 @@
#nullable enable
using JetBrains.Annotations;
using Robust.Shared.GameObjects;
namespace Content.Server.GameObjects.EntitySystems
{
/// <summary>
/// Used on the server side to manage global access level overrides.
/// </summary>
class ServerDoorSystem : EntitySystem
{
/// <summary>
/// Determines the base access behavior of all doors on the station.
/// </summary>
public AccessTypes AccessType { get; set; }
/// <summary>
/// How door access should be handled.
/// </summary>
public enum AccessTypes
{
/// <summary> ID based door access. </summary>
Id,
/// <summary>
/// Allows everyone to open doors, except external which airlocks are still handled with ID's
/// </summary>
AllowAllIdExternal,
/// <summary>
/// Allows everyone to open doors, except external airlocks which are never allowed, even if the user has
/// ID access.
/// </summary>
AllowAllNoExternal,
/// <summary> Allows everyone to open all doors. </summary>
AllowAll
}
public override void Initialize()
{
base.Initialize();
AccessType = AccessTypes.Id;
}
}
}

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Threading;
using Content.Server.GameObjects.Components.Suspicion;
using Content.Server.GameObjects.EntitySystems;
@@ -53,7 +53,7 @@ namespace Content.Server.GameTicking.GameRules
EntitySystem.Get<AudioSystem>().PlayGlobal("/Audio/Misc/tatoralert.ogg", AudioParams.Default, Predicate);
EntitySystem.Get<SuspicionEndTimerSystem>().EndTime = _endTime;
EntitySystem.Get<DoorSystem>().AccessType = DoorSystem.AccessTypes.AllowAllNoExternal;
EntitySystem.Get<ServerDoorSystem>().AccessType = ServerDoorSystem.AccessTypes.AllowAllNoExternal;
Timer.SpawnRepeating(DeadCheckDelay, CheckWinConditions, _checkTimerCancel.Token);
}
@@ -62,7 +62,7 @@ namespace Content.Server.GameTicking.GameRules
{
base.Removed();
EntitySystem.Get<DoorSystem>().AccessType = DoorSystem.AccessTypes.Id;
EntitySystem.Get<ServerDoorSystem>().AccessType = ServerDoorSystem.AccessTypes.Id;
EntitySystem.Get<SuspicionEndTimerSystem>().EndTime = null;
_checkTimerCancel.Cancel();

View File

@@ -0,0 +1,77 @@
#nullable enable
using Content.Shared.GameObjects.Components.Doors;
using Content.Shared.Interfaces.GameObjects.Components;
using System;
namespace Content.Server.Interfaces.GameObjects.Components.Doors
{
public interface IDoorCheck
{
/// <summary>
/// Called when the door's State variable is changed to a new variable that it was not equal to before.
/// </summary>
void OnStateChange(SharedDoorComponent.DoorState doorState) { }
/// <summary>
/// Called when the door is determining whether it is able to open.
/// </summary>
/// <returns>True if the door should open, false if it should not.</returns>
bool OpenCheck() => true;
/// <summary>
/// Called when the door is determining whether it is able to close.
/// </summary>
/// <returns>True if the door should close, false if it should not.</returns>
bool CloseCheck() => true;
/// <summary>
/// Called when the door is determining whether it is able to deny.
/// </summary>
/// <returns>True if the door should deny, false if it should not.</returns>
bool DenyCheck() => true;
/// <summary>
/// Whether the door's safety is on.
/// </summary>
/// <returns>True if safety is on, false if it is not.</returns>
bool SafetyCheck() => false;
/// <summary>
/// Whether the door should close automatically.
/// </summary>
/// <returns>True if the door should close automatically, false if it should not.</returns>
bool AutoCloseCheck() => false;
/// <summary>
/// Gets an override for the amount of time to pry open the door, or null if there is no override.
/// </summary>
/// <returns>Float if there is an override, null otherwise.</returns>
float? GetPryTime() => null;
/// <summary>
/// Gets an override for the amount of time before the door automatically closes, or null if there is no override.
/// </summary>
/// <returns>TimeSpan if there is an override, null otherwise.</returns>
TimeSpan? GetCloseSpeed() => null;
/// <summary>
/// A check to determine whether or not a click on the door should interact with it with the intent to open/close.
/// </summary>
/// <returns>True if the door's IActivate should not run, false otherwise.</returns>
bool BlockActivate(ActivateEventArgs eventArgs) => false;
/// <summary>
/// Called when somebody begins to pry open the door.
/// </summary>
/// <param name="eventArgs">The eventArgs of the InteractUsing method that called this function.</param>
void OnStartPry(InteractUsingEventArgs eventArgs) { }
/// <summary>
/// Check representing whether or not the door can be pried open.
/// </summary>
/// <param name="eventArgs">The eventArgs of the InteractUsing method that called this function.</param>
/// <returns>True if the door can be pried open, false if it cannot.</returns>
bool CanPryCheck(InteractUsingEventArgs eventArgs) => true;
}
}

View File

@@ -1,10 +1,181 @@
using System;
#nullable enable
using System;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Serialization;
using Robust.Shared.ViewVariables;
using Robust.Shared.Physics;
using System.Collections.Generic;
using Robust.Shared.Timing;
namespace Content.Shared.GameObjects.Components.Doors
{
[NetSerializable]
[Serializable]
public abstract class SharedDoorComponent : Component, ICollideSpecial
{
public override string Name => "Door";
public override uint? NetID => ContentNetIDs.DOOR;
[ComponentDependency]
protected readonly SharedAppearanceComponent? AppearanceComponent = null;
[ComponentDependency]
protected readonly IPhysicsComponent? PhysicsComponent = null;
[ViewVariables]
private DoorState _state = DoorState.Closed;
/// <summary>
/// The current state of the door -- whether it is open, closed, opening, or closing.
/// </summary>
public virtual DoorState State
{
get => _state;
protected set
{
if (_state == value)
{
return;
}
_state = value;
SetAppearance(State switch
{
DoorState.Open => DoorVisualState.Open,
DoorState.Closed => DoorVisualState.Closed,
DoorState.Opening => DoorVisualState.Opening,
DoorState.Closing => DoorVisualState.Closing,
_ => throw new ArgumentOutOfRangeException(),
});
}
}
/// <summary>
/// Closing time until impassable.
/// </summary>
protected TimeSpan CloseTimeOne;
/// <summary>
/// Closing time until fully closed.
/// </summary>
protected TimeSpan CloseTimeTwo;
/// <summary>
/// Opening time until passable.
/// </summary>
protected TimeSpan OpenTimeOne;
/// <summary>
/// Opening time until fully open.
/// </summary>
protected TimeSpan OpenTimeTwo;
/// <summary>
/// Time to finish denying.
/// </summary>
protected static TimeSpan DenyTime => TimeSpan.FromSeconds(0.45f);
/// <summary>
/// Used by ServerDoorComponent to get the CurTime for the client to use to know when to open, and by ClientDoorComponent to know the CurTime to correctly open.
/// </summary>
[Dependency] protected IGameTiming GameTiming = default!;
/// <summary>
/// The time the door began to open or close, if the door is opening or closing, or null if it is neither.
/// </summary>
protected TimeSpan? StateChangeStartTime = null;
/// <summary>
/// List of EntityUids of entities we're currently crushing. Cleared in OnPartialOpen().
/// </summary>
protected List<EntityUid> CurrentlyCrushing = new();
public override void ExposeData(ObjectSerializer serializer)
{
base.ExposeData(serializer);
serializer.DataReadWriteFunction(
"closeTimeOne",
0.4f,
seconds => CloseTimeOne = TimeSpan.FromSeconds(seconds),
() => CloseTimeOne.TotalSeconds);
serializer.DataReadWriteFunction(
"closeTimeTwo",
0.2f,
seconds => CloseTimeTwo = TimeSpan.FromSeconds(seconds),
() => CloseTimeOne.TotalSeconds);
serializer.DataReadWriteFunction(
"openTimeOne",
0.4f,
seconds => OpenTimeOne = TimeSpan.FromSeconds(seconds),
() => CloseTimeOne.TotalSeconds);
serializer.DataReadWriteFunction(
"openTimeTwo",
0.2f,
seconds => OpenTimeTwo = TimeSpan.FromSeconds(seconds),
() => CloseTimeOne.TotalSeconds);
}
protected void SetAppearance(DoorVisualState state)
{
if (AppearanceComponent != null)
{
AppearanceComponent.SetData(DoorVisuals.VisualState, state);
}
}
// stops us colliding with people we're crushing, to prevent hitbox clipping and jank
public bool PreventCollide(IPhysBody collidedwith)
{
return CurrentlyCrushing.Contains(collidedwith.Entity.Uid);
}
/// <summary>
/// Called when the door is partially opened.
/// </summary>
protected virtual void OnPartialOpen()
{
if (PhysicsComponent != null)
{
PhysicsComponent.CanCollide = false;
}
// we can't be crushing anyone anymore, since we're opening
CurrentlyCrushing.Clear();
}
/// <summary>
/// Called when the door is partially closed.
/// </summary>
protected virtual void OnPartialClose()
{
if (PhysicsComponent != null)
{
PhysicsComponent.CanCollide = true;
}
}
[Serializable, NetSerializable]
public enum DoorState
{
Open,
Closed,
Opening,
Closing,
}
}
[Serializable, NetSerializable]
public enum DoorVisualState
{
Open,
Closed,
Opening,
Closing,
Deny,
Welded
}
[Serializable, NetSerializable]
public enum DoorVisuals
{
VisualState,
@@ -12,15 +183,20 @@ namespace Content.Shared.GameObjects.Components.Doors
BoltLights
}
[NetSerializable]
[Serializable]
public enum DoorVisualState
[Serializable, NetSerializable]
public class DoorComponentState : ComponentState
{
Closed,
Opening,
Open,
Closing,
Deny,
Welded,
public readonly SharedDoorComponent.DoorState DoorState;
public readonly TimeSpan? StartTime;
public readonly List<EntityUid> CurrentlyCrushing;
public readonly TimeSpan CurTime;
public DoorComponentState(SharedDoorComponent.DoorState doorState, TimeSpan? startTime, List<EntityUid> currentlyCrushing, TimeSpan curTime) : base(ContentNetIDs.DOOR)
{
DoorState = doorState;
StartTime = startTime;
CurrentlyCrushing = currentlyCrushing;
CurTime = curTime;
}
}
}

View File

@@ -1,4 +1,4 @@
namespace Content.Shared.GameObjects
namespace Content.Shared.GameObjects
{
// Starting from 1000 to avoid crossover with engine.
public static class ContentNetIDs
@@ -91,6 +91,8 @@
public const uint DAMAGEABLE = 1084;
public const uint MAGBOOTS = 1085;
public const uint TAG = 1086;
// Used for clientside fake prediction of doors.
public const uint DOOR = 1087;
// Net IDs for integration tests.
public const uint PREDICTION_TEST = 10001;

View File

@@ -35,6 +35,7 @@
- MobImpassable
- VaultImpassable
- SmallImpassable
- type: Door
- type: Airlock
- type: Appearance
visuals:
@@ -77,7 +78,7 @@
parent: Airlock
name: glass airlock
components:
- type: Airlock
- type: Door
occludes: false
- type: Occluder
enabled: false

View File

@@ -1,11 +1,11 @@
# Airlocks
# Airlocks
- type: entity
parent: Airlock
id: AirlockExternal
suffix: External
description: "It opens, it closes, it might crush you, and there might be only space behind it.\nHas to be manually activated."
components:
- type: Airlock
- type: Door
bumpOpen: false
- type: Sprite
sprite: Constructible/Structures/Doors/airlock_external.rsi

View File

@@ -1,4 +1,4 @@
- type: entity
- type: entity
id: Firelock
name: firelock
description: Apply crowbar.
@@ -36,6 +36,13 @@
- MobImpassable
- VaultImpassable
- SmallImpassable
- type: Door
closeTimeOne: 0.1
closeTimeTwo: 0.6
openTimeOne: 0.1
openTimeTwo: 0.6
startOpen: true
bumpOpen: false
- type: Firelock
- type: Appearance
visuals:
@@ -70,7 +77,7 @@
parent: Firelock
name: glass firelock
components:
- type: Firelock
- type: Door
occludes: false
- type: Occluder
enabled: false
@@ -83,9 +90,9 @@
parent: Firelock
name: firelock
components:
- type: Firelock
- type: Door
occludes: false
canCrush: false
inhibitCrush: true
- type: Sprite
sprite: Constructible/Structures/Doors/edge_door_hazard.rsi
- type: Airtight