Predict doors and airlocks (#25419)

* predict doors and airlocks

* prying, too

* ack

* eek
This commit is contained in:
Nemanja
2024-02-22 18:01:31 -05:00
committed by GitHub
parent b7747596f1
commit ce0a51fc29
25 changed files with 444 additions and 559 deletions

View File

@@ -12,6 +12,9 @@ namespace Content.Shared.Doors.Components;
[Access(typeof(SharedAirlockSystem), Friend = AccessPermissions.ReadWriteExecute, Other = AccessPermissions.Read)]
public sealed partial class AirlockComponent : Component
{
[DataField, AutoNetworkedField]
public bool Powered;
// Need to network airlock safety state to avoid mis-predicts when a door auto-closes as the client walks through the door.
[ViewVariables(VVAccess.ReadWrite)]
[DataField, AutoNetworkedField]

View File

@@ -8,36 +8,43 @@ namespace Content.Shared.Doors.Components;
/// Companion component to DoorComponent that handles bolt-specific behavior.
/// </summary>
[RegisterComponent, NetworkedComponent]
[Access(typeof(SharedDoorBoltSystem))]
[Access(typeof(SharedDoorSystem))]
[AutoGenerateComponentState]
public sealed partial class DoorBoltComponent : Component
{
/// <summary>
/// Sound to play when the bolts on the airlock go up.
/// </summary>
[DataField("boltUpSound"), ViewVariables(VVAccess.ReadWrite)]
[DataField, ViewVariables(VVAccess.ReadWrite)]
public SoundSpecifier BoltUpSound = new SoundPathSpecifier("/Audio/Machines/boltsup.ogg");
/// <summary>
/// Sound to play when the bolts on the airlock go down.
/// </summary>
[DataField("boltDownSound"), ViewVariables(VVAccess.ReadWrite)]
[DataField, ViewVariables(VVAccess.ReadWrite)]
public SoundSpecifier BoltDownSound = new SoundPathSpecifier("/Audio/Machines/boltsdown.ogg");
/// <summary>
/// Whether the door bolts are currently deployed.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField, AutoNetworkedField]
public bool BoltsDown;
/// <summary>
/// Whether the bolt lights are currently enabled.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField, AutoNetworkedField]
public bool BoltLightsEnabled = true;
/// <summary>
/// True if the bolt wire is cut, which will force the airlock to always be bolted as long as it has power.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField, AutoNetworkedField]
public bool BoltWireCut;
/// <summary>
/// Used for prediction. true if the door has power.
/// </summary>
[DataField, AutoNetworkedField]
public bool Powered;
}

View File

@@ -124,14 +124,14 @@ public sealed partial class DoorComponent : Component
/// If false, this door is incapable of crushing entities. This just determines whether it will apply damage and
/// stun, not whether it can close despite entities being in the way.
/// </summary>
[DataField]
[DataField, AutoNetworkedField]
public bool CanCrush = true;
/// <summary>
/// Whether to check for colliding entities before closing. This may be overridden by other system by subscribing to
/// <see cref="BeforeDoorClosedEvent"/>. For example, hacked airlocks will set this to false.
/// </summary>
[DataField]
[DataField, AutoNetworkedField]
public bool PerformCollisionCheck = true;
/// <summary>
@@ -190,7 +190,7 @@ public sealed partial class DoorComponent : Component
/// The sprite state used for the door when it's being emagged.
/// </summary>
[DataField]
public string EmaggingSpriteState = "emagging";
public string EmaggingSpriteState = "sparks";
/// <summary>
/// The sprite state used for the door when it's open.

View File

@@ -1,5 +1,7 @@
using Content.Shared.Doors.Components;
using Content.Shared.Popups;
using Content.Shared.Prying.Components;
using Content.Shared.Wires;
namespace Content.Shared.Doors.Systems;
@@ -8,18 +10,112 @@ public abstract class SharedAirlockSystem : EntitySystem
[Dependency] protected readonly SharedAppearanceSystem Appearance = default!;
[Dependency] protected readonly SharedDoorSystem DoorSystem = default!;
[Dependency] protected readonly SharedPopupSystem Popup = default!;
[Dependency] private readonly SharedWiresSystem _wiresSystem = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<AirlockComponent, BeforeDoorClosedEvent>(OnBeforeDoorClosed);
SubscribeLocalEvent<AirlockComponent, DoorStateChangedEvent>(OnStateChanged);
SubscribeLocalEvent<AirlockComponent, BeforeDoorOpenedEvent>(OnBeforeDoorOpened);
SubscribeLocalEvent<AirlockComponent, BeforeDoorDeniedEvent>(OnBeforeDoorDenied);
SubscribeLocalEvent<AirlockComponent, GetPryTimeModifierEvent>(OnGetPryMod);
SubscribeLocalEvent<AirlockComponent, BeforePryEvent>(OnBeforePry);
}
protected virtual void OnBeforeDoorClosed(EntityUid uid, AirlockComponent airlock, BeforeDoorClosedEvent args)
private void OnBeforeDoorClosed(EntityUid uid, AirlockComponent airlock, BeforeDoorClosedEvent args)
{
if (args.Cancelled)
return;
if (!airlock.Safety)
args.PerformCollisionCheck = false;
// 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
&& !CanChangeState(uid, airlock))
{
args.Cancel();
}
}
private void OnStateChanged(EntityUid uid, AirlockComponent component, DoorStateChangedEvent args)
{
// Only show the maintenance panel if the airlock is closed
if (TryComp<WiresPanelComponent>(uid, out var wiresPanel))
{
_wiresSystem.ChangePanelVisibility(uid, wiresPanel, component.OpenPanelVisible || args.State != DoorState.Open);
}
// If the door is closed, we should look if the bolt was locked while closing
UpdateAutoClose(uid, component);
// Make sure the airlock auto closes again next time it is opened
if (args.State == DoorState.Closed)
component.AutoClose = true;
}
private void OnBeforeDoorOpened(EntityUid uid, AirlockComponent component, BeforeDoorOpenedEvent args)
{
if (!CanChangeState(uid, component))
args.Cancel();
}
private void OnBeforeDoorDenied(EntityUid uid, AirlockComponent component, BeforeDoorDeniedEvent args)
{
if (!CanChangeState(uid, component))
args.Cancel();
}
private void OnGetPryMod(EntityUid uid, AirlockComponent component, ref GetPryTimeModifierEvent args)
{
if (component.Powered)
args.PryTimeModifier *= component.PoweredPryModifier;
if (DoorSystem.IsBolted(uid))
args.PryTimeModifier *= component.BoltedPryModifier;
}
/// <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.AutoClose)
return;
if (!CanChangeState(uid, airlock))
return;
var autoev = new BeforeDoorAutoCloseEvent();
RaiseLocalEvent(uid, autoev);
if (autoev.Cancelled)
return;
DoorSystem.SetNextStateChange(uid, airlock.AutoCloseDelay * airlock.AutoCloseDelayModifier);
}
private void OnBeforePry(EntityUid uid, AirlockComponent component, ref BeforePryEvent args)
{
if (args.Cancelled)
return;
if (!component.Powered || args.PryPowered)
return;
args.Message = "airlock-component-cannot-pry-is-powered-message";
args.Cancelled = true;
}
public void UpdateEmergencyLightStatus(EntityUid uid, AirlockComponent component)
@@ -45,4 +141,9 @@ public abstract class SharedAirlockSystem : EntitySystem
{
component.Safety = value;
}
public bool CanChangeState(EntityUid uid, AirlockComponent component)
{
return component.Powered && !DoorSystem.IsBolted(uid);
}
}

View File

@@ -1,61 +0,0 @@
using Content.Shared.Doors.Components;
using Content.Shared.Popups;
using Content.Shared.Prying.Components;
using Robust.Shared.Audio;
using Robust.Shared.Audio.Systems;
namespace Content.Shared.Doors.Systems;
public abstract class SharedDoorBoltSystem : EntitySystem
{
[Dependency] protected readonly SharedAppearanceSystem Appearance = default!;
[Dependency] protected readonly SharedAudioSystem Audio = default!;
[Dependency] protected readonly SharedPopupSystem Popup = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<DoorBoltComponent, BeforeDoorOpenedEvent>(OnBeforeDoorOpened);
SubscribeLocalEvent<DoorBoltComponent, BeforeDoorClosedEvent>(OnBeforeDoorClosed);
SubscribeLocalEvent<DoorBoltComponent, BeforeDoorDeniedEvent>(OnBeforeDoorDenied);
SubscribeLocalEvent<DoorBoltComponent, BeforePryEvent>(OnDoorPry);
}
private void OnDoorPry(EntityUid uid, DoorBoltComponent component, ref BeforePryEvent args)
{
if (args.Cancelled)
return;
if (!component.BoltsDown || args.Force)
return;
args.Message = "airlock-component-cannot-pry-is-bolted-message";
args.Cancelled = true;
}
private void OnBeforeDoorOpened(EntityUid uid, DoorBoltComponent component, BeforeDoorOpenedEvent args)
{
if (component.BoltsDown)
args.Cancel();
}
private void OnBeforeDoorClosed(EntityUid uid, DoorBoltComponent component, BeforeDoorClosedEvent args)
{
if (component.BoltsDown)
args.Cancel();
}
private void OnBeforeDoorDenied(EntityUid uid, DoorBoltComponent component, BeforeDoorDeniedEvent args)
{
if (component.BoltsDown)
args.Cancel();
}
public void SetBoltWireCut(DoorBoltComponent component, bool value)
{
component.BoltWireCut = value;
}
}

View File

@@ -0,0 +1,109 @@
using Content.Shared.Doors.Components;
using Content.Shared.Prying.Components;
namespace Content.Shared.Doors.Systems;
public abstract partial class SharedDoorSystem
{
public void InitializeBolts()
{
base.Initialize();
SubscribeLocalEvent<DoorBoltComponent, BeforeDoorOpenedEvent>(OnBeforeDoorOpened);
SubscribeLocalEvent<DoorBoltComponent, BeforeDoorClosedEvent>(OnBeforeDoorClosed);
SubscribeLocalEvent<DoorBoltComponent, BeforeDoorDeniedEvent>(OnBeforeDoorDenied);
SubscribeLocalEvent<DoorBoltComponent, BeforePryEvent>(OnDoorPry);
SubscribeLocalEvent<DoorBoltComponent, DoorStateChangedEvent>(OnStateChanged);
}
private void OnDoorPry(EntityUid uid, DoorBoltComponent component, ref BeforePryEvent args)
{
if (args.Cancelled)
return;
if (!component.BoltsDown || args.Force)
return;
args.Message = "airlock-component-cannot-pry-is-bolted-message";
args.Cancelled = true;
}
private void OnBeforeDoorOpened(EntityUid uid, DoorBoltComponent component, BeforeDoorOpenedEvent args)
{
if (component.BoltsDown)
args.Cancel();
}
private void OnBeforeDoorClosed(EntityUid uid, DoorBoltComponent component, BeforeDoorClosedEvent args)
{
if (component.BoltsDown)
args.Cancel();
}
private void OnBeforeDoorDenied(EntityUid uid, DoorBoltComponent component, BeforeDoorDeniedEvent args)
{
if (component.BoltsDown)
args.Cancel();
}
public void SetBoltWireCut(Entity<DoorBoltComponent> ent, bool value)
{
ent.Comp.BoltWireCut = value;
Dirty(ent, ent.Comp);
}
public void UpdateBoltLightStatus(Entity<DoorBoltComponent> ent)
{
AppearanceSystem.SetData(ent, DoorVisuals.BoltLights, GetBoltLightsVisible(ent));
}
public bool GetBoltLightsVisible(Entity<DoorBoltComponent> ent)
{
return ent.Comp.BoltLightsEnabled &&
ent.Comp.BoltsDown &&
ent.Comp.Powered;
}
public void SetBoltLightsEnabled(Entity<DoorBoltComponent> ent, bool value)
{
if (ent.Comp.BoltLightsEnabled == value)
return;
ent.Comp.BoltLightsEnabled = value;
Dirty(ent, ent.Comp);
UpdateBoltLightStatus(ent);
}
public void SetBoltsDown(Entity<DoorBoltComponent> ent, bool value, EntityUid? user = null, bool predicted = false)
{
if (ent.Comp.BoltsDown == value)
return;
ent.Comp.BoltsDown = value;
Dirty(ent, ent.Comp);
UpdateBoltLightStatus(ent);
var sound = value ? ent.Comp.BoltDownSound : ent.Comp.BoltUpSound;
if (predicted)
Audio.PlayPredicted(sound, ent, user: user);
else
Audio.PlayPvs(sound, ent);
}
private void OnStateChanged(Entity<DoorBoltComponent> entity, ref DoorStateChangedEvent args)
{
// If the door is closed, we should look if the bolt was locked while closing
UpdateBoltLightStatus(entity);
}
public bool IsBolted(EntityUid uid, DoorBoltComponent? component = null)
{
if (!Resolve(uid, ref component))
{
return false;
}
return component.BoltsDown;
}
}

View File

@@ -1,26 +1,35 @@
using System.Linq;
using Content.Shared.Access.Components;
using Content.Shared.Access.Systems;
using Content.Shared.Administration.Logs;
using Content.Shared.Damage;
using Content.Shared.Database;
using Content.Shared.Doors.Components;
using Content.Shared.Emag.Systems;
using Content.Shared.Hands.Components;
using Content.Shared.Interaction;
using Content.Shared.Physics;
using Content.Shared.Popups;
using Content.Shared.Prying.Components;
using Content.Shared.Prying.Systems;
using Content.Shared.Stunnable;
using Content.Shared.Tag;
using Content.Shared.Tools.Systems;
using Robust.Shared.Audio;
using Robust.Shared.Physics.Components;
using Robust.Shared.Physics.Events;
using Robust.Shared.Physics.Systems;
using Robust.Shared.Timing;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Network;
namespace Content.Shared.Doors.Systems;
public abstract class SharedDoorSystem : EntitySystem
public abstract partial class SharedDoorSystem : EntitySystem
{
[Dependency] private readonly ISharedAdminLogManager _adminLog = default!;
[Dependency] protected readonly IGameTiming GameTiming = default!;
[Dependency] private readonly INetManager _net = default!;
[Dependency] protected readonly SharedPhysicsSystem PhysicsSystem = default!;
[Dependency] private readonly DamageableSystem _damageableSystem = default!;
[Dependency] private readonly SharedStunSystem _stunSystem = default!;
@@ -30,6 +39,11 @@ public abstract class SharedDoorSystem : EntitySystem
[Dependency] protected readonly SharedAppearanceSystem AppearanceSystem = default!;
[Dependency] private readonly OccluderSystem _occluder = default!;
[Dependency] private readonly AccessReaderSystem _accessReaderSystem = default!;
[Dependency] private readonly PryingSystem _pryingSystem = default!;
[Dependency] protected readonly SharedPopupSystem Popup = default!;
[ValidatePrototypeId<TagPrototype>]
public const string DoorBumpTag = "DoorBumpOpener";
/// <summary>
/// A body must have an intersection percentage larger than this in order to be considered as colliding with a
@@ -50,6 +64,8 @@ public abstract class SharedDoorSystem : EntitySystem
{
base.Initialize();
InitializeBolts();
SubscribeLocalEvent<DoorComponent, ComponentInit>(OnComponentInit);
SubscribeLocalEvent<DoorComponent, ComponentRemove>(OnRemove);
@@ -60,8 +76,12 @@ public abstract class SharedDoorSystem : EntitySystem
SubscribeLocalEvent<DoorComponent, StartCollideEvent>(HandleCollide);
SubscribeLocalEvent<DoorComponent, PreventCollideEvent>(PreventCollision);
SubscribeLocalEvent<DoorComponent, BeforePryEvent>(OnBeforePry);
SubscribeLocalEvent<DoorComponent, PriedEvent>(OnAfterPry);
SubscribeLocalEvent<DoorComponent, WeldableAttemptEvent>(OnWeldAttempt);
SubscribeLocalEvent<DoorComponent, WeldableChangedEvent>(OnWeldChanged);
SubscribeLocalEvent<DoorComponent, GetPryTimeModifierEvent>(OnPryTimeModifier);
SubscribeLocalEvent<DoorComponent, GotEmaggedEvent>(OnEmagged);
}
protected virtual void OnComponentInit(Entity<DoorComponent> ent, ref ComponentInit args)
@@ -100,6 +120,23 @@ public abstract class SharedDoorSystem : EntitySystem
_activeDoors.Remove(door);
}
private void OnEmagged(EntityUid uid, DoorComponent door, ref GotEmaggedEvent args)
{
if (!TryComp<AirlockComponent>(uid, out var airlock))
return;
if (IsBolted(uid) || !airlock.Powered)
return;
if (door.State == DoorState.Closed)
{
if (!SetState(uid, DoorState.Emagging, door))
return;
Audio.PlayPredicted(door.SparkSound, uid, args.UserUid, AudioParams.Default.WithVolume(8));
args.Handled = true;
}
}
#region StateManagement
private void OnHandleState(Entity<DoorComponent> ent, ref AfterAutoHandleStateEvent args)
{
@@ -113,14 +150,14 @@ public abstract class SharedDoorSystem : EntitySystem
AppearanceSystem.SetData(ent, DoorVisuals.State, door.State);
}
protected void SetState(EntityUid uid, DoorState state, DoorComponent? door = null)
protected bool SetState(EntityUid uid, DoorState state, DoorComponent? door = null)
{
if (!Resolve(uid, ref door))
return;
return false;
// If no change, return to avoid firing a new DoorStateChangedEvent.
if (state == door.State)
return;
return false;
switch (state)
{
@@ -159,14 +196,20 @@ public abstract class SharedDoorSystem : EntitySystem
Dirty(uid, door);
RaiseLocalEvent(uid, new DoorStateChangedEvent(state));
AppearanceSystem.SetData(uid, DoorVisuals.State, door.State);
return true;
}
#endregion
#region Interactions
protected virtual void OnActivate(EntityUid uid, DoorComponent door, ActivateInWorldEvent args)
protected void OnActivate(EntityUid uid, DoorComponent door, ActivateInWorldEvent args)
{
// avoid client-mispredicts, as the server will definitely handle this event
if (args.Handled || !door.ClickOpen)
return;
if (!TryToggleDoor(uid, door, args.User, predicted: true))
_pryingSystem.TryPry(uid, args.User, out _);
args.Handled = true;
}
@@ -181,6 +224,44 @@ public abstract class SharedDoorSystem : EntitySystem
args.Cancelled = true;
}
/// <summary>
/// Open or close a door after it has been successfully pried.
/// </summary>
private void OnAfterPry(EntityUid uid, DoorComponent door, ref PriedEvent args)
{
if (door.State == DoorState.Closed)
{
_adminLog.Add(LogType.Action, LogImpact.Medium, $"{ToPrettyString(args.User)} pried {ToPrettyString(uid)} open");
StartOpening(uid, door, args.User, true);
}
else if (door.State == DoorState.Open)
{
_adminLog.Add(LogType.Action, LogImpact.Medium, $"{ToPrettyString(args.User)} pried {ToPrettyString(uid)} closed");
StartClosing(uid, door, args.User, true);
}
}
private void OnWeldAttempt(EntityUid uid, DoorComponent component, WeldableAttemptEvent args)
{
if (component.CurrentlyCrushing.Count > 0)
{
args.Cancel();
return;
}
if (component.State != DoorState.Closed && component.State != DoorState.Welded)
{
args.Cancel();
}
}
private void OnWeldChanged(EntityUid uid, DoorComponent component, ref WeldableChangedEvent args)
{
if (component.State == DoorState.Closed)
SetState(uid, DoorState.Welded, component);
else if (component.State == DoorState.Welded)
SetState(uid, DoorState.Closed, component);
}
/// <summary>
/// Update the door state/visuals and play an access denied sound when a user without access interacts with the
/// door.
@@ -199,13 +280,15 @@ public abstract class SharedDoorSystem : EntitySystem
if (ev.Cancelled)
return;
SetState(uid, DoorState.Denying, door);
if (!SetState(uid, DoorState.Denying, door))
return;
if (door.DenySound != null)
PlaySound(uid, door.DenySound, AudioParams.Default.WithVolume(-3), user, predicted);
if (predicted)
Audio.PlayPredicted(door.DenySound, uid, user, AudioParams.Default.WithVolume(-3));
else if (_net.IsServer)
Audio.PlayPvs(door.DenySound, uid, AudioParams.Default.WithVolume(-3));
}
public bool TryToggleDoor(EntityUid uid, DoorComponent? door = null, EntityUid? user = null, bool predicted = false)
{
if (!Resolve(uid, ref door))
@@ -215,7 +298,8 @@ public abstract class SharedDoorSystem : EntitySystem
{
return TryOpen(uid, door, user, predicted, quiet: door.State == DoorState.Denying);
}
else if (door.State == DoorState.Open)
if (door.State == DoorState.Open)
{
return TryClose(uid, door, user, predicted);
}
@@ -254,7 +338,7 @@ public abstract class SharedDoorSystem : EntitySystem
if (!HasAccess(uid, user, door))
{
if (!quiet)
Deny(uid, door);
Deny(uid, door, user, predicted: true);
return false;
}
@@ -269,22 +353,29 @@ public abstract class SharedDoorSystem : EntitySystem
/// <param name="user"> The user (if any) opening the door</param>
/// <param name="predicted">Whether the interaction would have been
/// predicted. See comments in the PlaySound method on the Server system for details</param>
public virtual void StartOpening(EntityUid uid, DoorComponent? door = null, EntityUid? user = null, bool predicted = false)
public void StartOpening(EntityUid uid, DoorComponent? door = null, EntityUid? user = null, bool predicted = false)
{
if (!Resolve(uid, ref door))
return;
SetState(uid, DoorState.Opening, door);
var lastState = door.State;
if (door.OpenSound != null)
PlaySound(uid, door.OpenSound, AudioParams.Default.WithVolume(-5), user, predicted);
if (!SetState(uid, DoorState.Opening, door))
return;
if (predicted)
Audio.PlayPredicted(door.OpenSound, uid, user, AudioParams.Default.WithVolume(-5));
else if (_net.IsServer)
Audio.PlayPvs(door.OpenSound, uid, AudioParams.Default.WithVolume(-5));
if (lastState == DoorState.Emagging && TryComp<DoorBoltComponent>(uid, out var doorBoltComponent))
SetBoltsDown((uid, doorBoltComponent), !doorBoltComponent.BoltsDown, user, true);
// I'm not sure what the intent here is/was? It plays a sound if the user is opening a door with a hands
// component, but no actual hands!? What!? Is this the sound of them head-butting the door to get it to open??
// I'm 99% sure something is wrong here, but I kind of want to keep it this way.
if (user != null && TryComp(user.Value, out HandsComponent? hands) && hands.Hands.Count == 0)
PlaySound(uid, door.TryOpenDoorSound, AudioParams.Default.WithVolume(-2), user, predicted);
if (user != null && (!TryComp(user.Value, out HandsComponent? hands) || hands.Hands.Count == 0))
Audio.PlayPredicted(door.TryOpenDoorSound, uid, user, AudioParams.Default.WithVolume(-2));
}
/// <summary>
@@ -310,7 +401,7 @@ public abstract class SharedDoorSystem : EntitySystem
if (!Resolve(uid, ref door))
return false;
if (!CanClose(uid, door, user, false))
if (!CanClose(uid, door, user))
return false;
StartClosing(uid, door, user, predicted);
@@ -323,9 +414,7 @@ public abstract class SharedDoorSystem : EntitySystem
/// <param name="uid"> The uid of the door</param>
/// <param name="door"> The doorcomponent of the door</param>
/// <param name="user"> The user (if any) opening the door</param>
/// <param name="predicted">Whether the interaction would have been
/// predicted. See comments in the PlaySound method on the Server system for details</param>
public bool CanClose(EntityUid uid, DoorComponent? door = null, EntityUid? user = null, bool quiet = true)
public bool CanClose(EntityUid uid, DoorComponent? door = null, EntityUid? user = null)
{
if (!Resolve(uid, ref door))
return false;
@@ -336,7 +425,7 @@ public abstract class SharedDoorSystem : EntitySystem
return false;
var ev = new BeforeDoorClosedEvent(door.PerformCollisionCheck);
RaiseLocalEvent(uid, ev, false);
RaiseLocalEvent(uid, ev);
if (ev.Cancelled)
return false;
@@ -346,15 +435,18 @@ public abstract class SharedDoorSystem : EntitySystem
return !ev.PerformCollisionCheck || !GetColliding(uid).Any();
}
public virtual void StartClosing(EntityUid uid, DoorComponent? door = null, EntityUid? user = null, bool predicted = false)
public void StartClosing(EntityUid uid, DoorComponent? door = null, EntityUid? user = null, bool predicted = false)
{
if (!Resolve(uid, ref door))
return;
SetState(uid, DoorState.Closing, door);
if (!SetState(uid, DoorState.Closing, door))
return;
if (door.CloseSound != null)
PlaySound(uid, door.CloseSound, AudioParams.Default.WithVolume(-5), user, predicted);
if (predicted)
Audio.PlayPredicted(door.CloseSound, uid, user, AudioParams.Default.WithVolume(-5));
else if (_net.IsServer)
Audio.PlayPvs(door.CloseSound, uid, AudioParams.Default.WithVolume(-5));
}
/// <summary>
@@ -367,7 +459,6 @@ public abstract class SharedDoorSystem : EntitySystem
return false;
door.Partial = true;
Dirty(uid, door);
// Make sure no entity waled into the airlock when it started closing.
if (!CanClose(uid, door))
@@ -380,6 +471,7 @@ public abstract class SharedDoorSystem : EntitySystem
SetCollidable(uid, true, door, physics);
door.NextStateChange = GameTiming.CurTime + door.CloseTimeTwo;
Dirty(uid, door);
_activeDoors.Add((uid, door));
// Crush any entities. Note that we don't check airlock safety here. This should have been checked before
@@ -489,10 +581,22 @@ public abstract class SharedDoorSystem : EntitySystem
}
}
protected virtual void HandleCollide(EntityUid uid, DoorComponent door, ref StartCollideEvent args)
/// <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>
private void HandleCollide(EntityUid uid, DoorComponent door, ref 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 is not (DoorState.Closed or DoorState.Denying))
return;
var otherUid = args.OtherEntity;
if (Tags.HasTag(otherUid, DoorBumpTag))
TryOpen(uid, door, otherUid, quiet: door.State == DoorState.Denying);
}
#endregion
@@ -587,6 +691,19 @@ public abstract class SharedDoorSystem : EntitySystem
_activeDoors.Add((uid, door));
}
protected void CheckDoorBump(Entity<DoorComponent, PhysicsComponent> ent)
{
var (uid, door, physics) = ent;
if (door.BumpOpen)
{
foreach (var other in PhysicsSystem.GetContactingEntities(uid, physics, approximate: true))
{
if (Tags.HasTag(other, DoorBumpTag) && TryOpen(uid, door, other, quiet: true))
break;
}
}
}
/// <summary>
/// Iterate over active doors and progress them to the next state if they need to be updated.
/// </summary>
@@ -619,8 +736,6 @@ public abstract class SharedDoorSystem : EntitySystem
}
}
protected virtual void CheckDoorBump(Entity<DoorComponent, PhysicsComponent> ent) { }
/// <summary>
/// Makes a door proceed to the next state (if applicable).
/// </summary>
@@ -632,7 +747,7 @@ public abstract class SharedDoorSystem : EntitySystem
if (door.CurrentlyCrushing.Count > 0)
// This is a closed door that is crushing people and needs to auto-open. Note that we don't check "can open"
// here. The door never actually finished closing and we don't want people to get stuck inside of doors.
StartOpening(ent, door, predicted: true);
StartOpening(ent, door);
switch (door.State)
{
@@ -665,7 +780,7 @@ public abstract class SharedDoorSystem : EntitySystem
case DoorState.Open:
// This door is open, and queued for an auto-close.
if (!TryClose(ent, door, predicted: true))
if (!TryClose(ent, door))
{
// The door failed to close (blocked?). Try again in one second.
door.NextStateChange = time + TimeSpan.FromSeconds(1);
@@ -679,6 +794,4 @@ public abstract class SharedDoorSystem : EntitySystem
}
}
#endregion
protected abstract void PlaySound(EntityUid uid, SoundSpecifier soundSpecifier, AudioParams audioParams, EntityUid? predictingPlayer, bool predicted);
}