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:
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
62
Content.Client/GameObjects/EntitySystems/ClientDoorSystem.cs
Normal file
62
Content.Client/GameObjects/EntitySystems/ClientDoorSystem.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -31,7 +31,6 @@ namespace Content.Client
|
||||
"MeleeChemicalInjector",
|
||||
"Dice",
|
||||
"Construction",
|
||||
"Door",
|
||||
"PoweredLight",
|
||||
"Smes",
|
||||
"LightBulb",
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -1,103 +1,119 @@
|
||||
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;
|
||||
}
|
||||
|
||||
public override bool CanOpen()
|
||||
{
|
||||
return !IsHoldingFire() && !IsHoldingPressure() && base.CanOpen();
|
||||
}
|
||||
|
||||
public override bool CanClose(IEntity user) => true;
|
||||
public override bool CanOpen(IEntity user) => CanOpen();
|
||||
|
||||
public override async Task<bool> InteractUsing(InteractUsingEventArgs eventArgs)
|
||||
{
|
||||
if (await base.InteractUsing(eventArgs))
|
||||
return false;
|
||||
|
||||
if (!eventArgs.Using.TryGetComponent<ToolComponent>(out var tool))
|
||||
return false;
|
||||
|
||||
if (tool.HasQuality(ToolQuality.Prying) && !IsWeldedShut)
|
||||
{
|
||||
var holdingPressure = IsHoldingPressure();
|
||||
var holdingFire = IsHoldingFire();
|
||||
|
||||
if (State == DoorState.Closed)
|
||||
{
|
||||
if (holdingPressure)
|
||||
Owner.PopupMessage(eventArgs.User, "A gush of air blows in your face... Maybe you should reconsider.");
|
||||
}
|
||||
|
||||
if (IsWeldedShut || !await tool.UseTool(eventArgs.User, Owner, holdingPressure || holdingFire ? 1.5f : 0.25f, ToolQuality.Prying)) return false;
|
||||
if (State == DoorState.Closed)
|
||||
{
|
||||
Open();
|
||||
}
|
||||
else if (State == DoorState.Open)
|
||||
{
|
||||
Close();
|
||||
}
|
||||
|
||||
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool IDoorCheck.OpenCheck()
|
||||
{
|
||||
return !IsHoldingFire() && !IsHoldingPressure();
|
||||
}
|
||||
|
||||
bool IDoorCheck.DenyCheck() => false;
|
||||
|
||||
float? IDoorCheck.GetPryTime()
|
||||
{
|
||||
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;
|
||||
|
||||
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 (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 false;
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
44
Content.Server/GameObjects/EntitySystems/ServerDoorSystem.cs
Normal file
44
Content.Server/GameObjects/EntitySystems/ServerDoorSystem.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user