ECS Doors (#5887)
This commit is contained in:
@@ -1,9 +1,11 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using Content.Server.Doors.Systems;
|
||||
using Content.Server.Power.Components;
|
||||
using Content.Server.VendingMachines;
|
||||
using Content.Server.WireHacking;
|
||||
using Content.Shared.Doors;
|
||||
using Content.Shared.Doors.Components;
|
||||
using Content.Shared.Sound;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.GameObjects;
|
||||
@@ -18,10 +20,11 @@ using static Content.Shared.Wires.SharedWiresComponent.WiresAction;
|
||||
namespace Content.Server.Doors.Components
|
||||
{
|
||||
/// <summary>
|
||||
/// Companion component to ServerDoorComponent that handles airlock-specific behavior -- wires, requiring power to operate, bolts, and allowing automatic closing.
|
||||
/// Companion component to DoorComponent that handles airlock-specific behavior -- wires, requiring power to operate, bolts, and allowing automatic closing.
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public class AirlockComponent : Component, IWires
|
||||
[ComponentReference(typeof(SharedAirlockComponent))]
|
||||
public sealed class AirlockComponent : SharedAirlockComponent, IWires
|
||||
{
|
||||
[Dependency] private readonly IEntityManager _entityManager = default!;
|
||||
|
||||
@@ -88,7 +91,7 @@ namespace Content.Server.Doors.Components
|
||||
private bool BoltLightsVisible
|
||||
{
|
||||
get => _boltLightsWirePulsed && BoltsDown && IsPowered()
|
||||
&& _entityManager.TryGetComponent<ServerDoorComponent>(Owner, out var doorComponent) && doorComponent.State == SharedDoorComponent.DoorState.Closed;
|
||||
&& _entityManager.TryGetComponent<DoorComponent>(Owner, out var doorComponent) && doorComponent.State == DoorState.Closed;
|
||||
set
|
||||
{
|
||||
_boltLightsWirePulsed = value;
|
||||
@@ -96,18 +99,19 @@ namespace Content.Server.Doors.Components
|
||||
}
|
||||
}
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField("autoClose")]
|
||||
public bool AutoClose = true;
|
||||
/// <summary>
|
||||
/// Delay until an open door automatically closes.
|
||||
/// </summary>
|
||||
[DataField("autoCloseDelay")]
|
||||
public TimeSpan AutoCloseDelay = TimeSpan.FromSeconds(5f);
|
||||
|
||||
/// <summary>
|
||||
/// Multiplicative modifier for the auto-close delay. Can be modified by hacking the airlock wires. Setting to
|
||||
/// zero will disable auto-closing.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField("autoCloseDelayModifier")]
|
||||
public float AutoCloseDelayModifier = 1.0f;
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField("safety")]
|
||||
public bool Safety = true;
|
||||
|
||||
protected override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
@@ -168,19 +172,15 @@ namespace Content.Server.Doors.Components
|
||||
var boltLightsStatus = new StatusLightData(Color.Lime,
|
||||
_boltLightsWirePulsed ? StatusLightState.On : StatusLightState.Off, "BOLT LED");
|
||||
|
||||
var ev = new DoorGetCloseTimeModifierEvent();
|
||||
IoCManager.Resolve<IEntityManager>().EventBus.RaiseLocalEvent(Owner, ev, false);
|
||||
|
||||
var timingStatus =
|
||||
new StatusLightData(Color.Orange, !AutoClose ? StatusLightState.Off :
|
||||
!MathHelper.CloseToPercent(ev.CloseTimeModifier, 1.0f) ? StatusLightState.BlinkingSlow :
|
||||
new StatusLightData(Color.Orange, (AutoCloseDelayModifier <= 0) ? StatusLightState.Off :
|
||||
!MathHelper.CloseToPercent(AutoCloseDelayModifier, 1.0f) ? StatusLightState.BlinkingSlow :
|
||||
StatusLightState.On,
|
||||
"TIME");
|
||||
|
||||
var safetyStatus =
|
||||
new StatusLightData(Color.Red, Safety ? StatusLightState.On : StatusLightState.Off, "SAFETY");
|
||||
|
||||
|
||||
wiresComponent.SetStatus(AirlockWireStatus.PowerIndicator, powerLight);
|
||||
wiresComponent.SetStatus(AirlockWireStatus.BoltIndicator, boltStatus);
|
||||
wiresComponent.SetStatus(AirlockWireStatus.BoltLightIndicator, boltLightsStatus);
|
||||
@@ -212,17 +212,6 @@ namespace Content.Server.Doors.Components
|
||||
wiresComponent.IsWireCut(Wires.BackupPower);
|
||||
}
|
||||
|
||||
private void PowerDeviceOnOnPowerStateChanged(PowerChangedMessage e)
|
||||
{
|
||||
if (_entityManager.TryGetComponent<AppearanceComponent>(Owner, out var appearanceComponent))
|
||||
{
|
||||
appearanceComponent.SetData(DoorVisuals.Powered, e.Powered);
|
||||
}
|
||||
|
||||
// BoltLights also got out
|
||||
UpdateBoltLightStatus();
|
||||
}
|
||||
|
||||
private enum Wires
|
||||
{
|
||||
/// <summary>
|
||||
@@ -250,6 +239,7 @@ namespace Content.Server.Doors.Components
|
||||
BoltLight,
|
||||
|
||||
// Placeholder for when AI is implemented
|
||||
// aaaaany day now.
|
||||
AIControl,
|
||||
|
||||
/// <summary>
|
||||
@@ -281,7 +271,7 @@ namespace Content.Server.Doors.Components
|
||||
|
||||
public void WiresUpdate(WiresUpdateEventArgs args)
|
||||
{
|
||||
if (!_entityManager.TryGetComponent<ServerDoorComponent>(Owner, out var doorComponent))
|
||||
if (!_entityManager.TryGetComponent<DoorComponent>(Owner, out var doorComponent))
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -318,11 +308,13 @@ namespace Content.Server.Doors.Components
|
||||
BoltLightsVisible = !_boltLightsWirePulsed;
|
||||
break;
|
||||
case Wires.Timing:
|
||||
AutoCloseDelayModifier = 0.5f;
|
||||
doorComponent.RefreshAutoClose();
|
||||
// This is permanent, until the wire gets cut & mended.
|
||||
AutoCloseDelayModifier = 0.5f;
|
||||
EntitySystem.Get<AirlockSystem>().UpdateAutoClose(Owner, this);
|
||||
break;
|
||||
case Wires.Safety:
|
||||
Safety = !Safety;
|
||||
Dirty();
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -341,11 +333,12 @@ namespace Content.Server.Doors.Components
|
||||
BoltLightsVisible = true;
|
||||
break;
|
||||
case Wires.Timing:
|
||||
AutoClose = true;
|
||||
doorComponent.RefreshAutoClose();
|
||||
AutoCloseDelayModifier = 1;
|
||||
EntitySystem.Get<AirlockSystem>().UpdateAutoClose(Owner, this);
|
||||
break;
|
||||
case Wires.Safety:
|
||||
Safety = true;
|
||||
Dirty();
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -361,11 +354,12 @@ namespace Content.Server.Doors.Components
|
||||
BoltLightsVisible = false;
|
||||
break;
|
||||
case Wires.Timing:
|
||||
AutoClose = false;
|
||||
doorComponent.RefreshAutoClose();
|
||||
AutoCloseDelayModifier = 0; // disable auto close
|
||||
EntitySystem.Get<AirlockSystem>().UpdateAutoClose(Owner, this);
|
||||
break;
|
||||
case Wires.Safety:
|
||||
Safety = false;
|
||||
Dirty();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
using Robust.Shared.GameObjects;
|
||||
|
||||
namespace Content.Server.Doors.Components
|
||||
{
|
||||
[RegisterComponent]
|
||||
public class DoorBumpOpenerComponent : Component
|
||||
{
|
||||
public override string Name => "DoorBumpOpener";
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
using Content.Server.Atmos.Components;
|
||||
using Content.Server.Atmos.EntitySystems;
|
||||
using Content.Shared.Doors;
|
||||
using Content.Server.Doors.Systems;
|
||||
using Content.Shared.Doors.Components;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
@@ -27,9 +28,14 @@ namespace Content.Server.Doors.Components
|
||||
|
||||
public bool EmergencyPressureStop()
|
||||
{
|
||||
if (_entMan.TryGetComponent<ServerDoorComponent>(Owner, out var doorComp) && doorComp.State == SharedDoorComponent.DoorState.Open && doorComp.CanCloseGeneric())
|
||||
var doorSys = EntitySystem.Get<DoorSystem>();
|
||||
if (_entMan.TryGetComponent<DoorComponent>(Owner, out var door) &&
|
||||
door.State == DoorState.Open &&
|
||||
doorSys.CanClose(Owner, door))
|
||||
{
|
||||
doorComp.Close();
|
||||
doorSys.StartClosing(Owner, door);
|
||||
|
||||
// Door system also sets airtight, but only after a delay. We want it to be immediate.
|
||||
if (_entMan.TryGetComponent(Owner, out AirtightComponent? airtight))
|
||||
{
|
||||
EntitySystem.Get<AirtightSystem>().SetAirblocked(airtight, true);
|
||||
|
||||
@@ -1,774 +0,0 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Content.Server.Access;
|
||||
using Content.Server.Atmos.Components;
|
||||
using Content.Server.Atmos.EntitySystems;
|
||||
using Content.Server.Construction;
|
||||
using Content.Server.Construction.Components;
|
||||
using Content.Server.Hands.Components;
|
||||
using Content.Server.Stunnable;
|
||||
using Content.Server.Tools;
|
||||
using Content.Server.Tools.Components;
|
||||
using Content.Shared.Access.Components;
|
||||
using Content.Shared.Access.Systems;
|
||||
using Content.Shared.Damage;
|
||||
using Content.Shared.Doors;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Sound;
|
||||
using Content.Shared.Tools;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Physics;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
||||
using Robust.Shared.ViewVariables;
|
||||
using Timer = Robust.Shared.Timing.Timer;
|
||||
|
||||
namespace Content.Server.Doors.Components
|
||||
{
|
||||
[RegisterComponent]
|
||||
[ComponentReference(typeof(IActivate))]
|
||||
[ComponentReference(typeof(SharedDoorComponent))]
|
||||
public class ServerDoorComponent : SharedDoorComponent, IActivate, IInteractUsing, IMapInit
|
||||
{
|
||||
[Dependency] private readonly IEntityManager _entMan = default!;
|
||||
|
||||
[ViewVariables]
|
||||
[DataField("board", customTypeSerializer:typeof(PrototypeIdSerializer<EntityPrototype>))]
|
||||
private string? _boardPrototype;
|
||||
|
||||
[DataField("weldingQuality", customTypeSerializer:typeof(PrototypeIdSerializer<ToolQualityPrototype>))]
|
||||
private string _weldingQuality = "Welding";
|
||||
|
||||
[DataField("pryingQuality", customTypeSerializer:typeof(PrototypeIdSerializer<ToolQualityPrototype>))]
|
||||
private string _pryingQuality = "Prying";
|
||||
|
||||
[DataField("tryOpenDoorSound")]
|
||||
private SoundSpecifier _tryOpenDoorSound = new SoundPathSpecifier("/Audio/Effects/bang.ogg");
|
||||
|
||||
[DataField("crushDamage", required: true)]
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public DamageSpecifier CrushDamage = default!;
|
||||
|
||||
[DataField("changeAirtight")]
|
||||
public bool ChangeAirtight = true;
|
||||
|
||||
public override DoorState State
|
||||
{
|
||||
get => base.State;
|
||||
protected set
|
||||
{
|
||||
if (State == value)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
base.State = value;
|
||||
|
||||
StateChangeStartTime = State switch
|
||||
{
|
||||
DoorState.Open or DoorState.Closed => null,
|
||||
DoorState.Opening or DoorState.Closing => GameTiming.CurTime,
|
||||
_ => throw new ArgumentOutOfRangeException(),
|
||||
};
|
||||
|
||||
_entMan.EventBus.RaiseLocalEvent(Owner, new DoorStateChangedEvent(State), false);
|
||||
_autoCloseCancelTokenSource?.Cancel();
|
||||
|
||||
Dirty();
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly TimeSpan AutoCloseDelay = TimeSpan.FromSeconds(5);
|
||||
|
||||
private CancellationTokenSource? _stateChangeCancelTokenSource;
|
||||
private CancellationTokenSource? _autoCloseCancelTokenSource;
|
||||
|
||||
private const float DoorStunTime = 5f;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the door will ever crush.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)] [DataField("inhibitCrush")]
|
||||
private bool _inhibitCrush;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the door blocks light.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)] [DataField("occludes")]
|
||||
private bool _occludes = true;
|
||||
public bool Occludes => _occludes;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the door will open when it is bumped into.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)] [DataField("bumpOpen")]
|
||||
public bool BumpOpen = true;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the door will open when it is activated or clicked.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)] [DataField("clickOpen")]
|
||||
public bool ClickOpen = true;
|
||||
|
||||
/// <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>
|
||||
[ViewVariables(VVAccess.ReadWrite)] [DataField("startOpen")]
|
||||
private bool _startOpen = false;
|
||||
|
||||
/// <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>
|
||||
[DataField("welded")]
|
||||
private bool _isWeldedShut;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the airlock is welded shut.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public bool IsWeldedShut
|
||||
{
|
||||
get => _isWeldedShut;
|
||||
set
|
||||
{
|
||||
if (_isWeldedShut == value)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_isWeldedShut = value;
|
||||
SetAppearance(_isWeldedShut ? DoorVisualState.Welded : DoorVisualState.Closed);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Whether the door can ever be welded shut.
|
||||
/// </summary>
|
||||
[DataField("weldable")]
|
||||
private bool _weldable = true;
|
||||
|
||||
/// <summary>
|
||||
/// Sound to play when the door opens.
|
||||
/// </summary>
|
||||
[DataField("openSound")]
|
||||
public SoundSpecifier? OpenSound;
|
||||
|
||||
/// <summary>
|
||||
/// Sound to play when the door closes.
|
||||
/// </summary>
|
||||
[DataField("closeSound")]
|
||||
public SoundSpecifier? CloseSound;
|
||||
|
||||
/// <summary>
|
||||
/// Sound to play if the door is denied.
|
||||
/// </summary>
|
||||
[DataField("denySound")]
|
||||
public SoundSpecifier? DenySound;
|
||||
|
||||
/// <summary>
|
||||
/// Should this door automatically close if its been open for too long?
|
||||
/// </summary>
|
||||
[DataField("autoClose")]
|
||||
public bool AutoClose = true;
|
||||
|
||||
/// <summary>
|
||||
/// Default time that the door should take to pry open.
|
||||
/// </summary>
|
||||
[DataField("pryTime")]
|
||||
public float PryTime = 1.5f;
|
||||
|
||||
/// <summary>
|
||||
/// Minimum interval allowed between deny sounds in milliseconds.
|
||||
/// </summary>
|
||||
[DataField("denySoundMinimumInterval")]
|
||||
public float DenySoundMinimumInterval = 450.0f;
|
||||
|
||||
/// <summary>
|
||||
/// Used to stop people from spamming the deny sound.
|
||||
/// </summary>
|
||||
private TimeSpan LastDenySoundTime = TimeSpan.Zero;
|
||||
|
||||
/// <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)]
|
||||
//[DataField("canCrush")]
|
||||
//private bool _canCrush = true; // TODO implement door crushing
|
||||
|
||||
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.", _entMan.GetComponent<MetaDataComponent>(Owner).EntityName);
|
||||
return;
|
||||
}
|
||||
SetAppearance(DoorVisualState.Welded);
|
||||
}
|
||||
|
||||
CreateDoorElectronicsBoard();
|
||||
}
|
||||
|
||||
protected override void OnRemove()
|
||||
{
|
||||
_stateChangeCancelTokenSource?.Cancel();
|
||||
_autoCloseCancelTokenSource?.Cancel();
|
||||
|
||||
base.OnRemove();
|
||||
}
|
||||
|
||||
void IMapInit.MapInit()
|
||||
{
|
||||
if (_startOpen)
|
||||
{
|
||||
if (IsWeldedShut)
|
||||
{
|
||||
Logger.Warning("{0} prototype loaded with incompatible flags: 'welded' and 'startOpen' are both true.", _entMan.GetComponent<MetaDataComponent>(Owner).EntityName);
|
||||
return;
|
||||
}
|
||||
QuickOpen(false);
|
||||
}
|
||||
|
||||
CreateDoorElectronicsBoard();
|
||||
}
|
||||
|
||||
void IActivate.Activate(ActivateEventArgs eventArgs)
|
||||
{
|
||||
if (!ClickOpen)
|
||||
return;
|
||||
|
||||
var ev = new DoorClickShouldActivateEvent(eventArgs);
|
||||
_entMan.EventBus.RaiseLocalEvent(Owner, ev, false);
|
||||
if (ev.Handled)
|
||||
return;
|
||||
|
||||
if (State == DoorState.Open)
|
||||
{
|
||||
TryClose(eventArgs.User);
|
||||
}
|
||||
else if (State == DoorState.Closed)
|
||||
{
|
||||
TryOpen(eventArgs.User);
|
||||
}
|
||||
}
|
||||
|
||||
#region Opening
|
||||
|
||||
public void TryOpen(EntityUid? user = null)
|
||||
{
|
||||
var msg = new DoorOpenAttemptEvent();
|
||||
_entMan.EventBus.RaiseLocalEvent(Owner, msg);
|
||||
|
||||
if (msg.Cancelled) return;
|
||||
|
||||
if (user == null)
|
||||
{
|
||||
// a machine opened it or something, idk
|
||||
Open();
|
||||
}
|
||||
else if (CanOpenByEntity(user.Value))
|
||||
{
|
||||
Open();
|
||||
|
||||
if (_entMan.TryGetComponent(user, out HandsComponent? hands) && hands.Count == 0)
|
||||
{
|
||||
SoundSystem.Play(Filter.Pvs(Owner), _tryOpenDoorSound.GetSound(), Owner,
|
||||
AudioParams.Default.WithVolume(-2));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Deny();
|
||||
}
|
||||
}
|
||||
|
||||
public bool CanOpenByEntity(EntityUid user)
|
||||
{
|
||||
if (!CanOpenGeneric())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!_entMan.TryGetComponent(Owner, out AccessReaderComponent? access))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var doorSystem = EntitySystem.Get<DoorSystem>();
|
||||
var isAirlockExternal = HasAccessType("External");
|
||||
|
||||
var accessSystem = EntitySystem.Get<AccessReaderSystem>();
|
||||
return doorSystem.AccessType switch
|
||||
{
|
||||
DoorSystem.AccessTypes.AllowAll => true,
|
||||
DoorSystem.AccessTypes.AllowAllIdExternal => isAirlockExternal || accessSystem.IsAllowed(access, user),
|
||||
DoorSystem.AccessTypes.AllowAllNoExternal => !isAirlockExternal,
|
||||
_ => accessSystem.IsAllowed(access, 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 (_entMan.TryGetComponent(Owner, out AccessReaderComponent? 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;
|
||||
}
|
||||
|
||||
var ev = new BeforeDoorOpenedEvent();
|
||||
_entMan.EventBus.RaiseLocalEvent(Owner, ev, false);
|
||||
return !ev.Cancelled;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Opens the door. Does not check if this is possible.
|
||||
/// </summary>
|
||||
public void Open()
|
||||
{
|
||||
State = DoorState.Opening;
|
||||
if (Occludes && _entMan.TryGetComponent(Owner, out OccluderComponent? occluder))
|
||||
{
|
||||
occluder.Enabled = false;
|
||||
}
|
||||
|
||||
if (ChangeAirtight && _entMan.TryGetComponent(Owner, out AirtightComponent? airtight))
|
||||
{
|
||||
EntitySystem.Get<AirtightSystem>().SetAirblocked(airtight, false);
|
||||
}
|
||||
|
||||
_stateChangeCancelTokenSource?.Cancel();
|
||||
_stateChangeCancelTokenSource = new();
|
||||
|
||||
if (OpenSound != null)
|
||||
{
|
||||
SoundSystem.Play(Filter.Pvs(Owner), OpenSound.GetSound(), Owner,
|
||||
AudioParams.Default.WithVolume(-5));
|
||||
}
|
||||
|
||||
Owner.SpawnTimer(OpenTimeOne, async () =>
|
||||
{
|
||||
OnPartialOpen();
|
||||
await Timer.Delay(OpenTimeTwo, _stateChangeCancelTokenSource.Token);
|
||||
|
||||
State = DoorState.Open;
|
||||
RefreshAutoClose();
|
||||
}, _stateChangeCancelTokenSource.Token);
|
||||
}
|
||||
|
||||
protected override void OnPartialOpen()
|
||||
{
|
||||
base.OnPartialOpen();
|
||||
|
||||
if (ChangeAirtight && _entMan.TryGetComponent(Owner, out AirtightComponent? airtight))
|
||||
{
|
||||
EntitySystem.Get<AirtightSystem>().SetAirblocked(airtight, false);
|
||||
}
|
||||
|
||||
_entMan.EventBus.RaiseEvent(EventSource.Local, new AccessReaderChangeMessage(Owner, false));
|
||||
}
|
||||
|
||||
private void QuickOpen(bool refresh)
|
||||
{
|
||||
if (Occludes && _entMan.TryGetComponent(Owner, out OccluderComponent? occluder))
|
||||
{
|
||||
occluder.Enabled = false;
|
||||
}
|
||||
OnPartialOpen();
|
||||
State = DoorState.Open;
|
||||
if(refresh)
|
||||
RefreshAutoClose();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Closing
|
||||
|
||||
public void TryClose(EntityUid? user = null)
|
||||
{
|
||||
var msg = new DoorCloseAttemptEvent();
|
||||
_entMan.EventBus.RaiseLocalEvent(Owner, msg);
|
||||
|
||||
if (msg.Cancelled) return;
|
||||
|
||||
if (user != null && !CanCloseByEntity(user.Value))
|
||||
{
|
||||
Deny();
|
||||
return;
|
||||
}
|
||||
|
||||
Close();
|
||||
}
|
||||
|
||||
public bool CanCloseByEntity(EntityUid user)
|
||||
{
|
||||
if (!CanCloseGeneric())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!_entMan.TryGetComponent(Owner, out AccessReaderComponent? access))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var accessSystem = EntitySystem.Get<AccessReaderSystem>();
|
||||
return accessSystem.IsAllowed(access, 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()
|
||||
{
|
||||
var ev = new BeforeDoorClosedEvent();
|
||||
_entMan.EventBus.RaiseLocalEvent(Owner, ev, false);
|
||||
if (ev.Cancelled)
|
||||
return false;
|
||||
|
||||
return !IsSafetyColliding();
|
||||
}
|
||||
|
||||
private bool SafetyCheck()
|
||||
{
|
||||
var ev = new DoorSafetyEnabledEvent();
|
||||
_entMan.EventBus.RaiseLocalEvent(Owner, ev, false);
|
||||
return ev.Safety || _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()
|
||||
{
|
||||
var safety = SafetyCheck();
|
||||
|
||||
if (safety && _entMan.TryGetComponent(Owner, out PhysicsComponent? physicsComponent))
|
||||
{
|
||||
var broadPhaseSystem = EntitySystem.Get<SharedPhysicsSystem>();
|
||||
|
||||
// Use this version so we can ignore the CanCollide being false
|
||||
foreach(var _ in broadPhaseSystem.GetCollidingEntities(physicsComponent, -0.015f))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Closes the door. Does not check if this is possible.
|
||||
/// </summary>
|
||||
public void Close()
|
||||
{
|
||||
State = DoorState.Closing;
|
||||
|
||||
// no more autoclose; we ARE closed
|
||||
_autoCloseCancelTokenSource?.Cancel();
|
||||
|
||||
_stateChangeCancelTokenSource?.Cancel();
|
||||
_stateChangeCancelTokenSource = new();
|
||||
|
||||
if (CloseSound != null)
|
||||
{
|
||||
SoundSystem.Play(Filter.Pvs(Owner), CloseSound.GetSound(), Owner,
|
||||
AudioParams.Default.WithVolume(-5));
|
||||
}
|
||||
|
||||
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 && _entMan.TryGetComponent(Owner, out OccluderComponent? occluder))
|
||||
{
|
||||
occluder.Enabled = true;
|
||||
}
|
||||
|
||||
State = DoorState.Closed;
|
||||
}, _stateChangeCancelTokenSource.Token);
|
||||
}
|
||||
|
||||
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 = ChangeAirtight && (SafetyCheck() || !TryCrush());
|
||||
|
||||
if (becomeairtight && _entMan.TryGetComponent(Owner, out AirtightComponent? airtight))
|
||||
{
|
||||
EntitySystem.Get<AirtightSystem>().SetAirblocked(airtight, true);
|
||||
}
|
||||
|
||||
_entMan.EventBus.RaiseEvent(EventSource.Local, new AccessReaderChangeMessage(Owner, true));
|
||||
}
|
||||
|
||||
/// <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 (!_entMan.TryGetComponent(Owner, out PhysicsComponent physicsComponent) || physicsComponent is not IPhysBody body)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var collidingentities = body.GetCollidingEntities(Vector2.Zero, false);
|
||||
|
||||
if (!collidingentities.Any())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var doorAABB = body.GetWorldAABB();
|
||||
var hitsomebody = false;
|
||||
|
||||
// Crush
|
||||
foreach (var e in collidingentities)
|
||||
{
|
||||
var percentage = e.GetWorldAABB().IntersectPercentage(doorAABB);
|
||||
|
||||
if (percentage < 0.1f)
|
||||
continue;
|
||||
|
||||
hitsomebody = true;
|
||||
CurrentlyCrushing.Add(e.Owner);
|
||||
|
||||
if (_entMan.HasComponent<DamageableComponent>(e.Owner))
|
||||
EntitySystem.Get<DamageableSystem>().TryChangeDamage(e.Owner, CrushDamage);
|
||||
|
||||
EntitySystem.Get<StunSystem>().TryParalyze(e.Owner, TimeSpan.FromSeconds(DoorStunTime), true);
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
public void Deny()
|
||||
{
|
||||
var ev = new BeforeDoorDeniedEvent();
|
||||
_entMan.EventBus.RaiseLocalEvent(Owner, ev, false);
|
||||
if (ev.Cancelled)
|
||||
return;
|
||||
|
||||
if (State == DoorState.Open || IsWeldedShut)
|
||||
return;
|
||||
|
||||
if (DenySound != null)
|
||||
{
|
||||
if (LastDenySoundTime == TimeSpan.Zero)
|
||||
{
|
||||
LastDenySoundTime = _gameTiming.CurTime;
|
||||
}
|
||||
else
|
||||
{
|
||||
var difference = _gameTiming.CurTime - LastDenySoundTime;
|
||||
if (difference < TimeSpan.FromMilliseconds(DenySoundMinimumInterval))
|
||||
return;
|
||||
}
|
||||
|
||||
LastDenySoundTime = _gameTiming.CurTime;
|
||||
SoundSystem.Play(Filter.Pvs(Owner), DenySound.GetSound(), Owner,
|
||||
AudioParams.Default.WithVolume(-3));
|
||||
}
|
||||
|
||||
_stateChangeCancelTokenSource?.Cancel();
|
||||
_stateChangeCancelTokenSource = new();
|
||||
SetAppearance(DoorVisualState.Deny);
|
||||
Owner.SpawnTimer(DenyTime, () =>
|
||||
{
|
||||
SetAppearance(DoorVisualState.Closed);
|
||||
}, _stateChangeCancelTokenSource.Token);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Starts a new auto close timer if this is appropriate
|
||||
/// (i.e. event raised is not cancelled).
|
||||
/// </summary>
|
||||
public void RefreshAutoClose()
|
||||
{
|
||||
if (State != DoorState.Open)
|
||||
return;
|
||||
|
||||
if (!AutoClose)
|
||||
return;
|
||||
|
||||
var autoev = new BeforeDoorAutoCloseEvent();
|
||||
_entMan.EventBus.RaiseLocalEvent(Owner, autoev, false);
|
||||
if (autoev.Cancelled)
|
||||
return;
|
||||
|
||||
_autoCloseCancelTokenSource = new();
|
||||
|
||||
var ev = new DoorGetCloseTimeModifierEvent();
|
||||
_entMan.EventBus.RaiseLocalEvent(Owner, ev, false);
|
||||
var realCloseTime = AutoCloseDelay * ev.CloseTimeModifier;
|
||||
|
||||
Owner.SpawnRepeatingTimer(realCloseTime, async () =>
|
||||
{
|
||||
if (CanCloseGeneric())
|
||||
{
|
||||
// Close() cancels _autoCloseCancellationTokenSource, so we're fine.
|
||||
Close();
|
||||
}
|
||||
}, _autoCloseCancelTokenSource.Token);
|
||||
}
|
||||
|
||||
async Task<bool> IInteractUsing.InteractUsing(InteractUsingEventArgs eventArgs)
|
||||
{
|
||||
if(!_entMan.TryGetComponent(eventArgs.Using, out ToolComponent? tool))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var toolSystem = EntitySystem.Get<ToolSystem>();
|
||||
|
||||
// for prying doors
|
||||
if (tool.Qualities.Contains(_pryingQuality) && !IsWeldedShut)
|
||||
{
|
||||
var ev = new DoorGetPryTimeModifierEvent();
|
||||
_entMan.EventBus.RaiseLocalEvent(Owner, ev, false);
|
||||
|
||||
var canEv = new BeforeDoorPryEvent(eventArgs);
|
||||
_entMan.EventBus.RaiseLocalEvent(Owner, canEv, false);
|
||||
|
||||
if (canEv.Cancelled) return false;
|
||||
|
||||
var successfulPry = await toolSystem.UseTool(eventArgs.Using, eventArgs.User, Owner,
|
||||
0f, ev.PryTimeModifier * PryTime, _pryingQuality);
|
||||
|
||||
if (successfulPry && !IsWeldedShut)
|
||||
{
|
||||
_entMan.EventBus.RaiseLocalEvent(Owner, new OnDoorPryEvent(eventArgs), false);
|
||||
if (State == DoorState.Closed)
|
||||
{
|
||||
Open();
|
||||
}
|
||||
else if (State == DoorState.Open)
|
||||
{
|
||||
Close();
|
||||
}
|
||||
}
|
||||
|
||||
// regardless of whether this action actually succeeded, it generated a do-after bar. So prevent other
|
||||
// interactions from happening afterwards by returning true.
|
||||
return true;
|
||||
}
|
||||
|
||||
// for welding doors
|
||||
if (_beingWelded || !CanWeldShut || !_entMan.TryGetComponent(tool.Owner, out WelderComponent? welder) || !welder.Lit)
|
||||
{
|
||||
// no interaction occurred
|
||||
return false;
|
||||
}
|
||||
|
||||
_beingWelded = true;
|
||||
|
||||
// perform a do-after delay
|
||||
var result = await toolSystem.UseTool(eventArgs.Using, eventArgs.User, Owner, 3f, 3f, _weldingQuality, () => CanWeldShut);
|
||||
|
||||
// if successful, toggle the weld-status (while also ensuring that it can still be welded shut after the delay)
|
||||
if (result)
|
||||
IsWeldedShut = CanWeldShut && !IsWeldedShut;
|
||||
|
||||
_beingWelded = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates the corresponding door electronics board on the door.
|
||||
/// This exists so when you deconstruct doors that were serialized with the map,
|
||||
/// you can retrieve the door electronics board.
|
||||
/// </summary>
|
||||
private void CreateDoorElectronicsBoard()
|
||||
{
|
||||
// Ensure that the construction component is aware of the board container.
|
||||
if (_entMan.TryGetComponent(Owner, out ConstructionComponent? construction))
|
||||
EntitySystem.Get<ConstructionSystem>().AddContainer(Owner, "board", construction);
|
||||
|
||||
// We don't do anything if this is null or empty.
|
||||
if (string.IsNullOrEmpty(_boardPrototype))
|
||||
return;
|
||||
|
||||
var container = Owner.EnsureContainer<Container>("board", out var existed);
|
||||
|
||||
return;
|
||||
/* // TODO ShadowCommander: Re-enable when access is added to boards. Requires map update.
|
||||
if (existed)
|
||||
{
|
||||
// We already contain a board. Note: We don't check if it's the right one!
|
||||
if (container.ContainedEntities.Count != 0)
|
||||
return;
|
||||
}
|
||||
|
||||
var board = Owner.EntityManager.SpawnEntity(_boardPrototype, Owner.Transform.Coordinates);
|
||||
|
||||
if(!container.Insert(board))
|
||||
Logger.Warning($"Couldn't insert board {board} into door {Owner}!");
|
||||
*/
|
||||
}
|
||||
|
||||
public override ComponentState GetComponentState()
|
||||
{
|
||||
return new DoorComponentState(State, StateChangeStartTime, CurrentlyCrushing, GameTiming.CurTime);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,19 @@
|
||||
using Content.Server.Doors.Components;
|
||||
using Content.Server.Doors.Components;
|
||||
using Content.Server.Power.Components;
|
||||
using Content.Server.WireHacking;
|
||||
using Content.Shared.Doors;
|
||||
using Content.Shared.Doors.Components;
|
||||
using Content.Shared.Doors.Systems;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Popups;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Localization;
|
||||
using System;
|
||||
|
||||
namespace Content.Server.Doors.Systems
|
||||
{
|
||||
public class AirlockSystem : EntitySystem
|
||||
public sealed class AirlockSystem : SharedAirlockSystem
|
||||
{
|
||||
public override void Initialize()
|
||||
{
|
||||
@@ -19,12 +22,8 @@ namespace Content.Server.Doors.Systems
|
||||
SubscribeLocalEvent<AirlockComponent, PowerChangedEvent>(OnPowerChanged);
|
||||
SubscribeLocalEvent<AirlockComponent, DoorStateChangedEvent>(OnStateChanged);
|
||||
SubscribeLocalEvent<AirlockComponent, BeforeDoorOpenedEvent>(OnBeforeDoorOpened);
|
||||
SubscribeLocalEvent<AirlockComponent, BeforeDoorClosedEvent>(OnBeforeDoorClosed);
|
||||
SubscribeLocalEvent<AirlockComponent, BeforeDoorDeniedEvent>(OnBeforeDoorDenied);
|
||||
SubscribeLocalEvent<AirlockComponent, DoorSafetyEnabledEvent>(OnDoorSafetyCheck);
|
||||
SubscribeLocalEvent<AirlockComponent, BeforeDoorAutoCloseEvent>(OnDoorAutoCloseCheck);
|
||||
SubscribeLocalEvent<AirlockComponent, DoorGetCloseTimeModifierEvent>(OnDoorCloseTimeModifier);
|
||||
SubscribeLocalEvent<AirlockComponent, DoorClickShouldActivateEvent>(OnDoorClickShouldActivate);
|
||||
SubscribeLocalEvent<AirlockComponent, ActivateInWorldEvent>(OnActivate, before: new [] {typeof(DoorSystem)});
|
||||
SubscribeLocalEvent<AirlockComponent, BeforeDoorPryEvent>(OnDoorPry);
|
||||
}
|
||||
|
||||
@@ -35,21 +34,59 @@ namespace Content.Server.Doors.Systems
|
||||
appearanceComponent.SetData(DoorVisuals.Powered, args.Powered);
|
||||
}
|
||||
|
||||
if (!args.Powered)
|
||||
{
|
||||
// stop any scheduled auto-closing
|
||||
DoorSystem.SetNextStateChange(uid, null);
|
||||
}
|
||||
else
|
||||
{
|
||||
// door received power. Lets "wake" the door up, in case it is currently open and needs to auto-close.
|
||||
DoorSystem.SetNextStateChange(uid, TimeSpan.FromSeconds(1));
|
||||
}
|
||||
|
||||
// BoltLights also got out
|
||||
component.UpdateBoltLightStatus();
|
||||
}
|
||||
|
||||
private void OnStateChanged(EntityUid uid, AirlockComponent component, DoorStateChangedEvent args)
|
||||
{
|
||||
// TODO move to shared? having this be server-side, but having client-side door opening/closing & prediction
|
||||
// means that sometimes the panels & bolt lights may be visible despite a door being completely open.
|
||||
|
||||
// Only show the maintenance panel if the airlock is closed
|
||||
if (TryComp<WiresComponent>(uid, out var wiresComponent))
|
||||
{
|
||||
wiresComponent.IsPanelVisible =
|
||||
component.OpenPanelVisible
|
||||
|| args.State != SharedDoorComponent.DoorState.Open;
|
||||
|| args.State != DoorState.Open;
|
||||
}
|
||||
// If the door is closed, we should look if the bolt was locked while closing
|
||||
component.UpdateBoltLightStatus();
|
||||
|
||||
UpdateAutoClose(uid, component);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the auto close timer.
|
||||
/// </summary>
|
||||
public void UpdateAutoClose(EntityUid uid, AirlockComponent? airlock = null, DoorComponent? door = null)
|
||||
{
|
||||
if (!Resolve(uid, ref airlock, ref door))
|
||||
return;
|
||||
|
||||
if (door.State != DoorState.Open)
|
||||
return;
|
||||
|
||||
if (!airlock.CanChangeState())
|
||||
return;
|
||||
|
||||
var autoev = new BeforeDoorAutoCloseEvent();
|
||||
RaiseLocalEvent(uid, autoev, false);
|
||||
if (autoev.Cancelled)
|
||||
return;
|
||||
|
||||
DoorSystem.SetNextStateChange(uid, airlock.AutoCloseDelay * airlock.AutoCloseDelayModifier);
|
||||
}
|
||||
|
||||
private void OnBeforeDoorOpened(EntityUid uid, AirlockComponent component, BeforeDoorOpenedEvent args)
|
||||
@@ -58,10 +95,23 @@ namespace Content.Server.Doors.Systems
|
||||
args.Cancel();
|
||||
}
|
||||
|
||||
private void OnBeforeDoorClosed(EntityUid uid, AirlockComponent component, BeforeDoorClosedEvent args)
|
||||
protected override void OnBeforeDoorClosed(EntityUid uid, SharedAirlockComponent component, BeforeDoorClosedEvent args)
|
||||
{
|
||||
if (!component.CanChangeState())
|
||||
base.OnBeforeDoorClosed(uid, component, args);
|
||||
|
||||
if (args.Cancelled)
|
||||
return;
|
||||
|
||||
// only block based on bolts / power status when initially closing the door, not when its already
|
||||
// mid-transition. Particularly relevant for when the door was pried-closed with a crowbar, which bypasses
|
||||
// the initial power-check.
|
||||
|
||||
if (TryComp(uid, out DoorComponent? door)
|
||||
&& !door.Partial
|
||||
&& !Comp<AirlockComponent>(uid).CanChangeState())
|
||||
{
|
||||
args.Cancel();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnBeforeDoorDenied(EntityUid uid, AirlockComponent component, BeforeDoorDeniedEvent args)
|
||||
@@ -70,26 +120,10 @@ namespace Content.Server.Doors.Systems
|
||||
args.Cancel();
|
||||
}
|
||||
|
||||
private void OnDoorSafetyCheck(EntityUid uid, AirlockComponent component, DoorSafetyEnabledEvent args)
|
||||
{
|
||||
args.Safety = component.Safety;
|
||||
}
|
||||
|
||||
private void OnDoorAutoCloseCheck(EntityUid uid, AirlockComponent component, BeforeDoorAutoCloseEvent args)
|
||||
{
|
||||
if (!component.AutoClose)
|
||||
args.Cancel();
|
||||
}
|
||||
|
||||
private void OnDoorCloseTimeModifier(EntityUid uid, AirlockComponent component, DoorGetCloseTimeModifierEvent args)
|
||||
{
|
||||
args.CloseTimeModifier *= component.AutoCloseDelayModifier;
|
||||
}
|
||||
|
||||
private void OnDoorClickShouldActivate(EntityUid uid, AirlockComponent component, DoorClickShouldActivateEvent args)
|
||||
private void OnActivate(EntityUid uid, AirlockComponent component, ActivateInWorldEvent args)
|
||||
{
|
||||
if (TryComp<WiresComponent>(uid, out var wiresComponent) && wiresComponent.IsPanelOpen &&
|
||||
EntityManager.TryGetComponent(args.Args.User, out ActorComponent? actor))
|
||||
EntityManager.TryGetComponent(args.User, out ActorComponent? actor))
|
||||
{
|
||||
wiresComponent.OpenInterface(actor.PlayerSession);
|
||||
args.Handled = true;
|
||||
@@ -100,12 +134,12 @@ namespace Content.Server.Doors.Systems
|
||||
{
|
||||
if (component.IsBolted())
|
||||
{
|
||||
component.Owner.PopupMessage(args.Args.User, Loc.GetString("airlock-component-cannot-pry-is-bolted-message"));
|
||||
component.Owner.PopupMessage(args.User, Loc.GetString("airlock-component-cannot-pry-is-bolted-message"));
|
||||
args.Cancel();
|
||||
}
|
||||
if (component.IsPowered())
|
||||
{
|
||||
component.Owner.PopupMessage(args.Args.User, Loc.GetString("airlock-component-cannot-pry-is-powered-message"));
|
||||
component.Owner.PopupMessage(args.User, Loc.GetString("airlock-component-cannot-pry-is-powered-message"));
|
||||
args.Cancel();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,69 +1,293 @@
|
||||
using Content.Server.Doors.Components;
|
||||
using Content.Server.Access;
|
||||
using Content.Server.Atmos.Components;
|
||||
using Content.Server.Atmos.EntitySystems;
|
||||
using Content.Server.Construction;
|
||||
using Content.Server.Construction.Components;
|
||||
using Content.Server.Tools;
|
||||
using Content.Server.Tools.Components;
|
||||
using Content.Shared.Access.Components;
|
||||
using Content.Shared.Access.Systems;
|
||||
using Content.Shared.Doors;
|
||||
using Content.Shared.Doors.Components;
|
||||
using Content.Shared.Doors.Systems;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Tag;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Physics.Dynamics;
|
||||
using Robust.Shared.Player;
|
||||
using System.Linq;
|
||||
|
||||
namespace Content.Server.Doors
|
||||
namespace Content.Server.Doors.Systems;
|
||||
|
||||
public sealed class DoorSystem : SharedDoorSystem
|
||||
{
|
||||
/// <summary>
|
||||
/// Used on the server side to manage global access level overrides.
|
||||
/// </summary>
|
||||
internal sealed class DoorSystem : SharedDoorSystem
|
||||
[Dependency] private readonly ConstructionSystem _constructionSystem = default!;
|
||||
[Dependency] private readonly ToolSystem _toolSystem = default!;
|
||||
[Dependency] private readonly AirtightSystem _airtightSystem = default!;
|
||||
[Dependency] private readonly AccessReaderSystem _accessReaderSystem = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
/// <summary>
|
||||
/// Determines the base access behavior of all doors on the station.
|
||||
/// </summary>
|
||||
public AccessTypes AccessType { get; set; }
|
||||
base.Initialize();
|
||||
|
||||
/// <summary>
|
||||
/// How door access should be handled.
|
||||
/// </summary>
|
||||
public enum AccessTypes
|
||||
SubscribeLocalEvent<DoorComponent, MapInitEvent>(OnMapInit);
|
||||
SubscribeLocalEvent<DoorComponent, InteractUsingEvent>(OnInteractUsing);
|
||||
|
||||
SubscribeLocalEvent<DoorComponent, PryFinishedEvent>(OnPryFinished);
|
||||
SubscribeLocalEvent<DoorComponent, PryCancelledEvent>(OnPryCancelled);
|
||||
SubscribeLocalEvent<DoorComponent, WeldFinishedEvent>(OnWeldFinished);
|
||||
SubscribeLocalEvent<DoorComponent, WeldCancelledEvent>(OnWeldCancelled);
|
||||
}
|
||||
|
||||
// TODO AUDIO PREDICT Figure out a better way to handle sound and prediction. For now, this works well enough?
|
||||
//
|
||||
// Currently a client will predict when a door is going to close automatically. So any client in PVS range can just
|
||||
// play their audio locally. Playing it server-side causes an odd delay, while in shared it causes double-audio.
|
||||
//
|
||||
// But if we just do that, then if a door is closed prematurely as the result of an interaction (i.e., using "E" on
|
||||
// an open door), then the audio would only be played for the client performing the interaction.
|
||||
//
|
||||
// So we do this:
|
||||
// - Play audio client-side IF the closing is being predicted (auto-close or predicted interaction)
|
||||
// - Server assumes automated closing is predicted by clients and does not play audio unless otherwise specified.
|
||||
// - Major exception is player interactions, which other players cannot predict
|
||||
// - In that case, send audio to all players, except possibly the interacting player if it was a predicted
|
||||
// interaction.
|
||||
|
||||
/// <summary>
|
||||
/// Selectively send sound to clients, taking care to not send the double-audio.
|
||||
/// </summary>
|
||||
/// <param name="uid">The audio source</param>
|
||||
/// <param name="sound">The sound</param>
|
||||
/// <param name="predictingPlayer">The user (if any) that instigated an interaction</param>
|
||||
/// <param name="predicted">Whether this interaction would have been predicted. If the predicting player is null,
|
||||
/// this assumes it would have been predicted by all players in PVS range.</param>
|
||||
protected override void PlaySound(EntityUid uid, string sound, AudioParams audioParams, EntityUid? predictingPlayer, bool predicted)
|
||||
{
|
||||
// If this sound would have been predicted by all clients, do not play any audio.
|
||||
if (predicted && predictingPlayer == null)
|
||||
return;
|
||||
|
||||
var filter = Filter.Pvs(uid);
|
||||
|
||||
if (predicted)
|
||||
{
|
||||
/// <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
|
||||
// This interaction is predicted, but only by the instigating user, who will have played their own sounds.
|
||||
filter.RemoveWhereAttachedEntity(e => e == predictingPlayer);
|
||||
}
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
// send the sound to players.
|
||||
SoundSystem.Play(filter, sound, uid, AudioParams.Default.WithVolume(-5));
|
||||
}
|
||||
|
||||
AccessType = AccessTypes.Id;
|
||||
SubscribeLocalEvent<ServerDoorComponent, StartCollideEvent>(HandleCollide);
|
||||
#region DoAfters
|
||||
/// <summary>
|
||||
/// Weld or pry open a door.
|
||||
/// </summary>
|
||||
private void OnInteractUsing(EntityUid uid, DoorComponent door, InteractUsingEvent args)
|
||||
{
|
||||
if (args.Handled)
|
||||
return;
|
||||
|
||||
if (!TryComp(args.Used, out ToolComponent? tool))
|
||||
return;
|
||||
|
||||
if (tool.Qualities.Contains(door.PryingQuality))
|
||||
{
|
||||
TryPryDoor(uid, args.Used, args.User, door);
|
||||
args.Handled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
private void HandleCollide(EntityUid uid, ServerDoorComponent component, StartCollideEvent args)
|
||||
if (door.Weldable && tool.Qualities.Contains(door.WeldingQuality))
|
||||
{
|
||||
if (!EntityManager.HasComponent<DoorBumpOpenerComponent>(args.OtherFixture.Body.Owner))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (component.State != SharedDoorComponent.DoorState.Closed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!component.BumpOpen)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Disabled because it makes it suck hard to walk through double doors.
|
||||
|
||||
component.TryOpen(args.OtherFixture.Body.Owner);
|
||||
TryWeldDoor(uid, args.Used, args.User, door);
|
||||
args.Handled = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempt to weld a door shut, or unweld it if it is already welded. This does not actually check if the user
|
||||
/// is holding the correct tool.
|
||||
/// </summary>
|
||||
private async void TryWeldDoor(EntityUid target, EntityUid used, EntityUid user, DoorComponent door)
|
||||
{
|
||||
if (!door.Weldable || door.BeingWelded || door.CurrentlyCrushing.Count > 0)
|
||||
return;
|
||||
|
||||
// is the door in a weld-able state?
|
||||
if (door.State != DoorState.Closed && door.State != DoorState.Welded)
|
||||
return;
|
||||
|
||||
// perform a do-after delay
|
||||
door.BeingWelded = true;
|
||||
_toolSystem.UseTool(used, user, target, 3f, 3f, door.WeldingQuality,
|
||||
new WeldFinishedEvent(), new WeldCancelledEvent(), target);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pry open a door. This does not check if the user is holding the required tool.
|
||||
/// </summary>
|
||||
private async void TryPryDoor(EntityUid target, EntityUid tool, EntityUid user, DoorComponent door)
|
||||
{
|
||||
if (door.State == DoorState.Welded)
|
||||
return;
|
||||
|
||||
var canEv = new BeforeDoorPryEvent(user);
|
||||
RaiseLocalEvent(target, canEv, false);
|
||||
|
||||
if (canEv.Cancelled)
|
||||
return;
|
||||
|
||||
var modEv = new DoorGetPryTimeModifierEvent();
|
||||
RaiseLocalEvent(target, modEv, false);
|
||||
|
||||
_toolSystem.UseTool(tool, user, target, 0f, modEv.PryTimeModifier * door.PryTime, door.PryingQuality,
|
||||
new PryFinishedEvent(), new PryCancelledEvent(), target);
|
||||
}
|
||||
|
||||
private void OnWeldCancelled(EntityUid uid, DoorComponent door, WeldCancelledEvent args)
|
||||
{
|
||||
door.BeingWelded = false;
|
||||
}
|
||||
|
||||
private void OnWeldFinished(EntityUid uid, DoorComponent door, WeldFinishedEvent args)
|
||||
{
|
||||
door.BeingWelded = false;
|
||||
|
||||
if (!door.Weldable)
|
||||
return;
|
||||
|
||||
if (door.State == DoorState.Closed)
|
||||
SetState(uid, DoorState.Welded, door);
|
||||
else if (door.State == DoorState.Welded)
|
||||
SetState(uid, DoorState.Closed, door);
|
||||
}
|
||||
|
||||
private void OnPryCancelled(EntityUid uid, DoorComponent door, PryCancelledEvent args)
|
||||
{
|
||||
door.BeingPried = false;
|
||||
}
|
||||
|
||||
private void OnPryFinished(EntityUid uid, DoorComponent door, PryFinishedEvent args)
|
||||
{
|
||||
door.BeingPried = false;
|
||||
|
||||
if (door.State == DoorState.Closed)
|
||||
StartOpening(uid, door);
|
||||
else if (door.State == DoorState.Open)
|
||||
StartClosing(uid, door);
|
||||
}
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Does the user have the permissions required to open this door?
|
||||
/// </summary>
|
||||
public override bool HasAccess(EntityUid uid, EntityUid? user = null, AccessReaderComponent? access = null)
|
||||
{
|
||||
// TODO network AccessComponent for predicting doors
|
||||
|
||||
// if there is no "user" we skip the access checks. Access is also ignored in some game-modes.
|
||||
if (user == null || AccessType == AccessTypes.AllowAll)
|
||||
return true;
|
||||
|
||||
if (!Resolve(uid, ref access, false))
|
||||
return true;
|
||||
|
||||
var isExternal = access.AccessLists.Any(list => list.Contains("External"));
|
||||
|
||||
return AccessType switch
|
||||
{
|
||||
// Some game modes modify access rules.
|
||||
AccessTypes.AllowAllIdExternal => !isExternal || _accessReaderSystem.IsAllowed(access, user.Value),
|
||||
AccessTypes.AllowAllNoExternal => !isExternal,
|
||||
_ => _accessReaderSystem.IsAllowed(access, user.Value)
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Open a door if a player or door-bumper (PDA, ID-card) collide with the door. Sadly, bullets no longer
|
||||
/// generate "access denied" sounds as you fire at a door.
|
||||
/// </summary>
|
||||
protected override void HandleCollide(EntityUid uid, DoorComponent door, StartCollideEvent args)
|
||||
{
|
||||
// TODO ACCESS READER move access reader to shared and predict door opening/closing
|
||||
// Then this can be moved to the shared system without mispredicting.
|
||||
if (!door.BumpOpen)
|
||||
return;
|
||||
|
||||
if (door.State != DoorState.Closed)
|
||||
return;
|
||||
|
||||
if (TryComp(args.OtherFixture.Body.Owner, out TagComponent? tags) && tags.HasTag("DoorBumpOpener"))
|
||||
TryOpen(uid, door, args.OtherFixture.Body.Owner);
|
||||
}
|
||||
|
||||
public override void OnPartialOpen(EntityUid uid, DoorComponent? door = null, PhysicsComponent? physics = null)
|
||||
{
|
||||
if (!Resolve(uid, ref door, ref physics))
|
||||
return;
|
||||
|
||||
base.OnPartialOpen(uid, door, physics);
|
||||
|
||||
if (door.ChangeAirtight && TryComp(uid, out AirtightComponent? airtight))
|
||||
{
|
||||
_airtightSystem.SetAirblocked(airtight, false);
|
||||
}
|
||||
|
||||
// Path-finding. Has nothing directly to do with access readers.
|
||||
RaiseLocalEvent(new AccessReaderChangeMessage(uid, false));
|
||||
}
|
||||
|
||||
public override bool OnPartialClose(EntityUid uid, DoorComponent? door = null, PhysicsComponent? physics = null)
|
||||
{
|
||||
if (!Resolve(uid, ref door, ref physics))
|
||||
return false;
|
||||
|
||||
if (!base.OnPartialClose(uid, door, physics))
|
||||
return false;
|
||||
|
||||
// update airtight, if we did not crush something.
|
||||
if (door.ChangeAirtight && door.CurrentlyCrushing.Count == 0 && TryComp(uid, out AirtightComponent? airtight))
|
||||
_airtightSystem.SetAirblocked(airtight, true);
|
||||
|
||||
// Path-finding. Has nothing directly to do with access readers.
|
||||
RaiseLocalEvent(new AccessReaderChangeMessage(uid, true));
|
||||
return true;
|
||||
}
|
||||
|
||||
private void OnMapInit(EntityUid uid, DoorComponent door, MapInitEvent args)
|
||||
{
|
||||
// Ensure that the construction component is aware of the board container.
|
||||
if (TryComp(uid, out ConstructionComponent? construction))
|
||||
_constructionSystem.AddContainer(uid, "board", construction);
|
||||
|
||||
// We don't do anything if this is null or empty.
|
||||
if (string.IsNullOrEmpty(door.BoardPrototype))
|
||||
return;
|
||||
|
||||
var container = uid.EnsureContainer<Container>("board", out var existed);
|
||||
|
||||
/* // TODO ShadowCommander: Re-enable when access is added to boards. Requires map update.
|
||||
if (existed)
|
||||
{
|
||||
// We already contain a board. Note: We don't check if it's the right one!
|
||||
if (container.ContainedEntities.Count != 0)
|
||||
return;
|
||||
}
|
||||
|
||||
var board = Owner.EntityManager.SpawnEntity(_boardPrototype, Owner.Transform.Coordinates);
|
||||
|
||||
if(!container.Insert(board))
|
||||
Logger.Warning($"Couldn't insert board {board} into door {Owner}!");
|
||||
*/
|
||||
}
|
||||
}
|
||||
|
||||
public class PryFinishedEvent : EntityEventArgs { }
|
||||
public class PryCancelledEvent : EntityEventArgs { }
|
||||
public class WeldFinishedEvent : EntityEventArgs { }
|
||||
public class WeldCancelledEvent : EntityEventArgs { }
|
||||
|
||||
@@ -1,17 +1,21 @@
|
||||
using Content.Server.Atmos.Monitor.Components;
|
||||
using Content.Server.Atmos.Monitor.Components;
|
||||
using Content.Server.Atmos.Monitor.Systems;
|
||||
using Content.Server.Doors.Components;
|
||||
using Content.Shared.Atmos.Monitor;
|
||||
using Content.Shared.Doors;
|
||||
using Content.Shared.Doors.Components;
|
||||
using Content.Shared.Doors.Systems;
|
||||
using Content.Shared.Popups;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Log;
|
||||
|
||||
namespace Content.Server.Doors.Systems
|
||||
{
|
||||
public class FirelockSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly SharedDoorSystem _doorSystem = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
@@ -19,8 +23,8 @@ namespace Content.Server.Doors.Systems
|
||||
SubscribeLocalEvent<FirelockComponent, BeforeDoorOpenedEvent>(OnBeforeDoorOpened);
|
||||
SubscribeLocalEvent<FirelockComponent, BeforeDoorDeniedEvent>(OnBeforeDoorDenied);
|
||||
SubscribeLocalEvent<FirelockComponent, DoorGetPryTimeModifierEvent>(OnDoorGetPryTimeModifier);
|
||||
SubscribeLocalEvent<FirelockComponent, DoorClickShouldActivateEvent>(OnDoorClickShouldActivate);
|
||||
SubscribeLocalEvent<FirelockComponent, BeforeDoorPryEvent>(OnBeforeDoorPry);
|
||||
|
||||
SubscribeLocalEvent<FirelockComponent, BeforeDoorAutoCloseEvent>(OnBeforeDoorAutoclose);
|
||||
SubscribeLocalEvent<FirelockComponent, AtmosMonitorAlarmEvent>(OnAtmosAlarm);
|
||||
}
|
||||
@@ -42,26 +46,20 @@ namespace Content.Server.Doors.Systems
|
||||
args.PryTimeModifier *= component.LockedPryTimeModifier;
|
||||
}
|
||||
|
||||
private void OnDoorClickShouldActivate(EntityUid uid, FirelockComponent component, DoorClickShouldActivateEvent args)
|
||||
{
|
||||
// We're a firelock, you can't click to open it
|
||||
args.Handled = true;
|
||||
}
|
||||
|
||||
private void OnBeforeDoorPry(EntityUid uid, FirelockComponent component, BeforeDoorPryEvent args)
|
||||
{
|
||||
if (!TryComp<ServerDoorComponent>(uid, out var doorComponent) || doorComponent.State != SharedDoorComponent.DoorState.Closed)
|
||||
if (!TryComp<DoorComponent>(uid, out var door) || door.State != DoorState.Closed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (component.IsHoldingPressure())
|
||||
{
|
||||
component.Owner.PopupMessage(args.Args.User, Loc.GetString("firelock-component-is-holding-pressure-message"));
|
||||
component.Owner.PopupMessage(args.User, Loc.GetString("firelock-component-is-holding-pressure-message"));
|
||||
}
|
||||
else if (component.IsHoldingFire())
|
||||
{
|
||||
component.Owner.PopupMessage(args.Args.User, Loc.GetString("firelock-component-is-holding-fire-message"));
|
||||
component.Owner.PopupMessage(args.User, Loc.GetString("firelock-component-is-holding-fire-message"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,12 +79,12 @@ namespace Content.Server.Doors.Systems
|
||||
|
||||
private void OnAtmosAlarm(EntityUid uid, FirelockComponent component, AtmosMonitorAlarmEvent args)
|
||||
{
|
||||
if (!TryComp<ServerDoorComponent>(uid, out var doorComponent)) return;
|
||||
if (!TryComp<DoorComponent>(uid, out var doorComponent)) return;
|
||||
|
||||
if (args.HighestNetworkType == AtmosMonitorAlarmType.Normal)
|
||||
{
|
||||
if (doorComponent.State == SharedDoorComponent.DoorState.Closed)
|
||||
doorComponent.Open();
|
||||
if (doorComponent.State == DoorState.Closed)
|
||||
_doorSystem.TryOpen(uid);
|
||||
}
|
||||
else if (args.HighestNetworkType == AtmosMonitorAlarmType.Danger)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user