This commit is contained in:
Leon Friedrich
2022-01-30 13:49:56 +13:00
committed by GitHub
parent 6fb492aae7
commit c465715273
37 changed files with 1400 additions and 1429 deletions

View File

@@ -0,0 +1,8 @@
using Content.Shared.Doors.Components;
using Robust.Shared.GameObjects;
namespace Content.Client.Doors;
[RegisterComponent]
[ComponentReference(typeof(SharedAirlockComponent))]
public sealed class AirlockComponent : SharedAirlockComponent { }

View File

@@ -0,0 +1,5 @@
using Content.Shared.Doors.Systems;
namespace Content.Client.Doors;
public sealed class AirlockSystem : SharedAirlockSystem { }

View File

@@ -1,6 +1,6 @@
using System; using System;
using Content.Client.Wires.Visualizers; using Content.Client.Wires.Visualizers;
using Content.Shared.Doors; using Content.Shared.Doors.Components;
using JetBrains.Annotations; using JetBrains.Annotations;
using Robust.Client.Animations; using Robust.Client.Animations;
using Robust.Client.GameObjects; using Robust.Client.GameObjects;
@@ -8,6 +8,7 @@ using Robust.Shared.GameObjects;
using Robust.Shared.IoC; using Robust.Shared.IoC;
using Robust.Shared.Serialization; using Robust.Shared.Serialization;
using Robust.Shared.Serialization.Manager.Attributes; using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.Timing;
namespace Content.Client.Doors namespace Content.Client.Doors
{ {
@@ -15,6 +16,7 @@ namespace Content.Client.Doors
public class AirlockVisualizer : AppearanceVisualizer, ISerializationHooks public class AirlockVisualizer : AppearanceVisualizer, ISerializationHooks
{ {
[Dependency] private readonly IEntityManager _entMan = default!; [Dependency] private readonly IEntityManager _entMan = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
private const string AnimationKey = "airlock_animation"; private const string AnimationKey = "airlock_animation";
@@ -122,15 +124,20 @@ namespace Content.Client.Doors
public override void OnChangeData(AppearanceComponent component) public override void OnChangeData(AppearanceComponent component)
{ {
// only start playing animations once.
if (!_gameTiming.IsFirstTimePredicted)
return;
base.OnChangeData(component); base.OnChangeData(component);
var sprite = _entMan.GetComponent<ISpriteComponent>(component.Owner); var sprite = _entMan.GetComponent<ISpriteComponent>(component.Owner);
var animPlayer = _entMan.GetComponent<AnimationPlayerComponent>(component.Owner); var animPlayer = _entMan.GetComponent<AnimationPlayerComponent>(component.Owner);
if (!component.TryGetData(DoorVisuals.VisualState, out DoorVisualState state)) if (!component.TryGetData(DoorVisuals.State, out DoorState state))
{ {
state = DoorVisualState.Closed; state = DoorState.Closed;
} }
var door = _entMan.GetComponent<DoorComponent>(component.Owner);
var unlitVisible = true; var unlitVisible = true;
var boltedVisible = false; var boltedVisible = false;
var weldedVisible = false; var weldedVisible = false;
@@ -141,7 +148,7 @@ namespace Content.Client.Doors
} }
switch (state) switch (state)
{ {
case DoorVisualState.Open: case DoorState.Open:
sprite.LayerSetState(DoorVisualLayers.Base, "open"); sprite.LayerSetState(DoorVisualLayers.Base, "open");
unlitVisible = _openUnlitVisible; unlitVisible = _openUnlitVisible;
if (_openUnlitVisible && !_simpleVisuals) if (_openUnlitVisible && !_simpleVisuals)
@@ -149,7 +156,7 @@ namespace Content.Client.Doors
sprite.LayerSetState(DoorVisualLayers.BaseUnlit, "open_unlit"); sprite.LayerSetState(DoorVisualLayers.BaseUnlit, "open_unlit");
} }
break; break;
case DoorVisualState.Closed: case DoorState.Closed:
sprite.LayerSetState(DoorVisualLayers.Base, "closed"); sprite.LayerSetState(DoorVisualLayers.Base, "closed");
if (!_simpleVisuals) if (!_simpleVisuals)
{ {
@@ -157,17 +164,19 @@ namespace Content.Client.Doors
sprite.LayerSetState(DoorVisualLayers.BaseBolted, "bolted_unlit"); sprite.LayerSetState(DoorVisualLayers.BaseBolted, "bolted_unlit");
} }
break; break;
case DoorVisualState.Opening: case DoorState.Opening:
animPlayer.Play(OpenAnimation, AnimationKey); animPlayer.Play(OpenAnimation, AnimationKey);
break; break;
case DoorVisualState.Closing: case DoorState.Closing:
if (door.CurrentlyCrushing.Count == 0)
animPlayer.Play(CloseAnimation, AnimationKey); animPlayer.Play(CloseAnimation, AnimationKey);
else
sprite.LayerSetState(DoorVisualLayers.Base, "closed");
break; break;
case DoorVisualState.Deny: case DoorState.Denying:
if (!animPlayer.HasRunningAnimation(AnimationKey))
animPlayer.Play(DenyAnimation, AnimationKey); animPlayer.Play(DenyAnimation, AnimationKey);
break; break;
case DoorVisualState.Welded: case DoorState.Welded:
weldedVisible = true; weldedVisible = true;
break; break;
default: default:
@@ -185,7 +194,7 @@ namespace Content.Client.Doors
if (!_simpleVisuals) if (!_simpleVisuals)
{ {
sprite.LayerSetVisible(DoorVisualLayers.BaseUnlit, unlitVisible && state != DoorVisualState.Closed); sprite.LayerSetVisible(DoorVisualLayers.BaseUnlit, unlitVisible && state != DoorState.Closed && state != DoorState.Welded);
sprite.LayerSetVisible(DoorVisualLayers.BaseWelded, weldedVisible); sprite.LayerSetVisible(DoorVisualLayers.BaseWelded, weldedVisible);
sprite.LayerSetVisible(DoorVisualLayers.BaseBolted, unlitVisible && boltedVisible); sprite.LayerSetVisible(DoorVisualLayers.BaseBolted, unlitVisible && boltedVisible);
} }

View File

@@ -1,100 +0,0 @@
using System;
using Content.Shared.Doors;
using JetBrains.Annotations;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
using DrawDepthTag = Robust.Shared.GameObjects.DrawDepth;
using DrawDepth = Content.Shared.DrawDepth.DrawDepth;
namespace Content.Client.Doors
{
/// <summary>
/// Bare-bones client-side door component; used to stop door-based mispredicts.
/// </summary>
[UsedImplicitly]
[RegisterComponent]
[ComponentReference(typeof(SharedDoorComponent))]
public class ClientDoorComponent : SharedDoorComponent
{
[DataField("openDrawDepth", customTypeSerializer: typeof(ConstantSerializer<DrawDepthTag>))]
public int OpenDrawDepth = (int) DrawDepth.Doors;
[DataField("closedDrawDepth", customTypeSerializer: typeof(ConstantSerializer<DrawDepthTag>))]
public int ClosedDrawDepth = (int) DrawDepth.Doors;
private bool _stateChangeHasProgressed = false;
private TimeSpan _timeOffset;
public override DoorState State
{
protected set
{
if (State == value)
{
return;
}
base.State = value;
IoCManager.Resolve<IEntityManager>().EventBus.RaiseLocalEvent(Owner, new DoorStateChangedEvent(State), false);
}
}
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();
}
}
}
}

View File

@@ -1,69 +1,35 @@
using System; using Content.Shared.Doors.Components;
using System.Collections.Generic; using Content.Shared.Doors.Systems;
using Content.Shared.Doors;
using Robust.Client.GameObjects; using Robust.Client.GameObjects;
using Robust.Shared.Audio;
using Robust.Shared.GameObjects; using Robust.Shared.GameObjects;
using static Content.Shared.Doors.SharedDoorComponent; using Robust.Shared.Player;
namespace Content.Client.Doors namespace Content.Client.Doors;
public sealed class DoorSystem : SharedDoorSystem
{ {
/// <summary> // Gotta love it when both the client-side and server-side sprite components both have a draw depth, but for
/// Used by the client to "predict" when doors will change how collideable they are as part of their opening / closing. // whatever bloody reason the shared component doesn't.
/// </summary> protected override void UpdateAppearance(EntityUid uid, DoorComponent? door = null)
internal sealed class DoorSystem : SharedDoorSystem
{ {
/// <summary> if (!Resolve(uid, ref door))
/// List of doors that need to be periodically checked.
/// </summary>
private readonly List<ClientDoorComponent> _activeDoors = new();
public override void Initialize()
{
base.Initialize();
UpdatesOutsidePrediction = true;
SubscribeLocalEvent<ClientDoorComponent, DoorStateChangedEvent>(OnDoorStateChanged);
}
private void OnDoorStateChanged(EntityUid uid, ClientDoorComponent door, DoorStateChangedEvent args)
{
switch (args.State)
{
case DoorState.Closed:
case DoorState.Open:
_activeDoors.Remove(door);
break;
case DoorState.Closing:
case DoorState.Opening:
_activeDoors.Add(door);
break;
default:
throw new ArgumentOutOfRangeException();
}
if (!EntityManager.TryGetComponent(uid, out SpriteComponent sprite))
return; return;
// Update sprite draw depth. If the door is opening or closing, we will use the closed-draw depth. base.UpdateAppearance(uid, door);
sprite.DrawDepth = (args.State == DoorState.Open)
if (TryComp(uid, out SpriteComponent? sprite))
{
sprite.DrawDepth = (door.State == DoorState.Open)
? door.OpenDrawDepth ? door.OpenDrawDepth
: door.ClosedDrawDepth; : door.ClosedDrawDepth;
} }
}
/// <inheritdoc /> // TODO AUDIO PREDICT see comments in server-side PlaySound()
public override void Update(float frameTime) protected override void PlaySound(EntityUid uid, string sound, AudioParams audioParams, EntityUid? predictingPlayer, bool predicted)
{ {
for (var i = _activeDoors.Count - 1; i >= 0; i--) if (GameTiming.InPrediction && GameTiming.IsFirstTimePredicted)
{ SoundSystem.Play(Filter.Local(), sound, uid, audioParams);
var comp = _activeDoors[i];
if (comp.Deleted)
{
_activeDoors.RemoveAt(i);
continue;
}
comp.OnUpdate();
}
}
} }
} }

View File

@@ -44,7 +44,6 @@ namespace Content.Client.Entry
"ResearchPointSource", "ResearchPointSource",
"ResearchClient", "ResearchClient",
"IdCardConsole", "IdCardConsole",
"Airlock",
"ThermalRegulator", "ThermalRegulator",
"AtmosFixMarker", "AtmosFixMarker",
"CablePlacer", "CablePlacer",
@@ -75,7 +74,6 @@ namespace Content.Client.Entry
"Brain", "Brain",
"CommunicationsConsole", "CommunicationsConsole",
"BarSign", "BarSign",
"DoorBumpOpener",
"SolarPanel", "SolarPanel",
"BodyScanner", "BodyScanner",
"Stunbaton", "Stunbaton",

View File

@@ -1,10 +1,10 @@
using System; using System;
using System.Threading.Tasks; using System.Threading.Tasks;
using Content.Server.Doors.Components; using Content.Server.Doors.Components;
using Content.Shared.Doors; using Content.Server.Doors.Systems;
using Content.Shared.Doors.Components;
using NUnit.Framework; using NUnit.Framework;
using Robust.Shared.GameObjects; using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map; using Robust.Shared.Map;
using Robust.Shared.Physics; using Robust.Shared.Physics;
@@ -57,7 +57,7 @@ namespace Content.IntegrationTests.Tests.Doors
var entityManager = server.ResolveDependency<IEntityManager>(); var entityManager = server.ResolveDependency<IEntityManager>();
EntityUid airlock = default; EntityUid airlock = default;
ServerDoorComponent doorComponent = null; DoorComponent doorComponent = null;
server.Assert(() => server.Assert(() =>
{ {
@@ -66,32 +66,32 @@ namespace Content.IntegrationTests.Tests.Doors
airlock = entityManager.SpawnEntity("AirlockDummy", MapCoordinates.Nullspace); airlock = entityManager.SpawnEntity("AirlockDummy", MapCoordinates.Nullspace);
Assert.True(entityManager.TryGetComponent(airlock, out doorComponent)); Assert.True(entityManager.TryGetComponent(airlock, out doorComponent));
Assert.That(doorComponent.State, Is.EqualTo(SharedDoorComponent.DoorState.Closed)); Assert.That(doorComponent.State, Is.EqualTo(DoorState.Closed));
}); });
await server.WaitIdleAsync(); await server.WaitIdleAsync();
server.Assert(() => server.Assert(() =>
{ {
doorComponent.Open(); EntitySystem.Get<DoorSystem>().StartOpening(airlock);
Assert.That(doorComponent.State, Is.EqualTo(SharedDoorComponent.DoorState.Opening)); Assert.That(doorComponent.State, Is.EqualTo(DoorState.Opening));
}); });
await server.WaitIdleAsync(); await server.WaitIdleAsync();
await WaitUntil(server, () => doorComponent.State == SharedDoorComponent.DoorState.Open); await WaitUntil(server, () => doorComponent.State == DoorState.Open);
Assert.That(doorComponent.State, Is.EqualTo(SharedDoorComponent.DoorState.Open)); Assert.That(doorComponent.State, Is.EqualTo(DoorState.Open));
server.Assert(() => server.Assert(() =>
{ {
doorComponent.Close(); EntitySystem.Get<DoorSystem>().TryClose((EntityUid) airlock);
Assert.That(doorComponent.State, Is.EqualTo(SharedDoorComponent.DoorState.Closing)); Assert.That(doorComponent.State, Is.EqualTo(DoorState.Closing));
}); });
await WaitUntil(server, () => doorComponent.State == SharedDoorComponent.DoorState.Closed); await WaitUntil(server, () => doorComponent.State == DoorState.Closed);
Assert.That(doorComponent.State, Is.EqualTo(SharedDoorComponent.DoorState.Closed)); Assert.That(doorComponent.State, Is.EqualTo(DoorState.Closed));
server.Assert(() => server.Assert(() =>
{ {
@@ -123,7 +123,7 @@ namespace Content.IntegrationTests.Tests.Doors
IPhysBody physBody = null; IPhysBody physBody = null;
EntityUid physicsDummy = default; EntityUid physicsDummy = default;
EntityUid airlock = default; EntityUid airlock = default;
ServerDoorComponent doorComponent = null; DoorComponent doorComponent = null;
var physicsDummyStartingX = -1; var physicsDummyStartingX = -1;
@@ -139,7 +139,7 @@ namespace Content.IntegrationTests.Tests.Doors
Assert.True(entityManager.TryGetComponent(physicsDummy, out physBody)); Assert.True(entityManager.TryGetComponent(physicsDummy, out physBody));
Assert.True(entityManager.TryGetComponent(airlock, out doorComponent)); Assert.True(entityManager.TryGetComponent(airlock, out doorComponent));
Assert.That(doorComponent.State, Is.EqualTo(SharedDoorComponent.DoorState.Closed)); Assert.That(doorComponent.State, Is.EqualTo(DoorState.Closed));
}); });
await server.WaitIdleAsync(); await server.WaitIdleAsync();

View File

@@ -1,8 +1,9 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Content.Server.Doors.Components; using Content.Server.Doors.Components;
using Content.Shared.Access.Components; using Content.Shared.Access.Components;
using Content.Shared.Doors.Components;
using Robust.Shared.GameObjects; using Robust.Shared.GameObjects;
using Robust.Shared.IoC; using Robust.Shared.IoC;
using Robust.Shared.Map; using Robust.Shared.Map;
@@ -262,7 +263,7 @@ namespace Content.Server.AI.Pathfinding
{ {
var entMan = IoCManager.Resolve<IEntityManager>(); var entMan = IoCManager.Resolve<IEntityManager>();
// If we're a door // If we're a door
if (entMan.HasComponent<AirlockComponent>(entity) || entMan.HasComponent<ServerDoorComponent>(entity)) if (entMan.HasComponent<AirlockComponent>(entity) || entMan.HasComponent<DoorComponent>(entity))
{ {
// If we need access to traverse this then add to readers, otherwise no point adding it (except for maybe tile costs in future) // If we need access to traverse this then add to readers, otherwise no point adding it (except for maybe tile costs in future)
// TODO: Check for powered I think (also need an event for when it's depowered // TODO: Check for powered I think (also need an event for when it's depowered

View File

@@ -1,6 +1,6 @@
using System.Collections.Generic; using System.Collections.Generic;
using Content.Server.Doors.Components;
using Content.Shared.Construction; using Content.Shared.Construction;
using Content.Shared.Doors.Components;
using Content.Shared.Examine; using Content.Shared.Examine;
using JetBrains.Annotations; using JetBrains.Annotations;
using Robust.Shared.GameObjects; using Robust.Shared.GameObjects;
@@ -19,10 +19,10 @@ namespace Content.Server.Construction.Conditions
public bool Condition(EntityUid uid, IEntityManager entityManager) public bool Condition(EntityUid uid, IEntityManager entityManager)
{ {
if (!entityManager.TryGetComponent(uid, out ServerDoorComponent? doorComponent)) if (!entityManager.TryGetComponent(uid, out DoorComponent? doorComponent))
return false; return false;
return doorComponent.IsWeldedShut == Welded; return doorComponent.State == DoorState.Welded;
} }
public bool DoExamine(ExaminedEvent args) public bool DoExamine(ExaminedEvent args)
@@ -31,9 +31,10 @@ namespace Content.Server.Construction.Conditions
var entMan = IoCManager.Resolve<IEntityManager>(); var entMan = IoCManager.Resolve<IEntityManager>();
if (!entMan.TryGetComponent(entity, out ServerDoorComponent? door)) return false; if (!entMan.TryGetComponent(entity, out DoorComponent? door)) return false;
if (door.IsWeldedShut != Welded) var isWelded = door.State == DoorState.Welded;
if (isWelded != Welded)
{ {
if (Welded == true) if (Welded == true)
args.PushMarkup(Loc.GetString("construction-examine-condition-door-weld", ("entityName", entMan.GetComponent<MetaDataComponent>(entity).EntityName)) + "\n"); args.PushMarkup(Loc.GetString("construction-examine-condition-door-weld", ("entityName", entMan.GetComponent<MetaDataComponent>(entity).EntityName)) + "\n");

View File

@@ -1,9 +1,11 @@
using System; using System;
using System.Threading; using System.Threading;
using Content.Server.Doors.Systems;
using Content.Server.Power.Components; using Content.Server.Power.Components;
using Content.Server.VendingMachines; using Content.Server.VendingMachines;
using Content.Server.WireHacking; using Content.Server.WireHacking;
using Content.Shared.Doors; using Content.Shared.Doors;
using Content.Shared.Doors.Components;
using Content.Shared.Sound; using Content.Shared.Sound;
using Robust.Shared.Audio; using Robust.Shared.Audio;
using Robust.Shared.GameObjects; using Robust.Shared.GameObjects;
@@ -18,10 +20,11 @@ using static Content.Shared.Wires.SharedWiresComponent.WiresAction;
namespace Content.Server.Doors.Components namespace Content.Server.Doors.Components
{ {
/// <summary> /// <summary>
/// Companion component to ServerDoorComponent that handles airlock-specific behavior -- wires, requiring power to operate, bolts, and allowing automatic closing. /// Companion component to DoorComponent that handles airlock-specific behavior -- wires, requiring power to operate, bolts, and allowing automatic closing.
/// </summary> /// </summary>
[RegisterComponent] [RegisterComponent]
public class AirlockComponent : Component, IWires [ComponentReference(typeof(SharedAirlockComponent))]
public sealed class AirlockComponent : SharedAirlockComponent, IWires
{ {
[Dependency] private readonly IEntityManager _entityManager = default!; [Dependency] private readonly IEntityManager _entityManager = default!;
@@ -88,7 +91,7 @@ namespace Content.Server.Doors.Components
private bool BoltLightsVisible private bool BoltLightsVisible
{ {
get => _boltLightsWirePulsed && BoltsDown && IsPowered() get => _boltLightsWirePulsed && BoltsDown && IsPowered()
&& _entityManager.TryGetComponent<ServerDoorComponent>(Owner, out var doorComponent) && doorComponent.State == SharedDoorComponent.DoorState.Closed; && _entityManager.TryGetComponent<DoorComponent>(Owner, out var doorComponent) && doorComponent.State == DoorState.Closed;
set set
{ {
_boltLightsWirePulsed = value; _boltLightsWirePulsed = value;
@@ -96,18 +99,19 @@ namespace Content.Server.Doors.Components
} }
} }
[ViewVariables(VVAccess.ReadWrite)] /// <summary>
[DataField("autoClose")] /// Delay until an open door automatically closes.
public bool AutoClose = true; /// </summary>
[DataField("autoCloseDelay")]
public TimeSpan AutoCloseDelay = TimeSpan.FromSeconds(5f);
/// <summary>
/// Multiplicative modifier for the auto-close delay. Can be modified by hacking the airlock wires. Setting to
/// zero will disable auto-closing.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)] [ViewVariables(VVAccess.ReadWrite)]
[DataField("autoCloseDelayModifier")]
public float AutoCloseDelayModifier = 1.0f; public float AutoCloseDelayModifier = 1.0f;
[ViewVariables(VVAccess.ReadWrite)]
[DataField("safety")]
public bool Safety = true;
protected override void Initialize() protected override void Initialize()
{ {
base.Initialize(); base.Initialize();
@@ -168,19 +172,15 @@ namespace Content.Server.Doors.Components
var boltLightsStatus = new StatusLightData(Color.Lime, var boltLightsStatus = new StatusLightData(Color.Lime,
_boltLightsWirePulsed ? StatusLightState.On : StatusLightState.Off, "BOLT LED"); _boltLightsWirePulsed ? StatusLightState.On : StatusLightState.Off, "BOLT LED");
var ev = new DoorGetCloseTimeModifierEvent();
IoCManager.Resolve<IEntityManager>().EventBus.RaiseLocalEvent(Owner, ev, false);
var timingStatus = var timingStatus =
new StatusLightData(Color.Orange, !AutoClose ? StatusLightState.Off : new StatusLightData(Color.Orange, (AutoCloseDelayModifier <= 0) ? StatusLightState.Off :
!MathHelper.CloseToPercent(ev.CloseTimeModifier, 1.0f) ? StatusLightState.BlinkingSlow : !MathHelper.CloseToPercent(AutoCloseDelayModifier, 1.0f) ? StatusLightState.BlinkingSlow :
StatusLightState.On, StatusLightState.On,
"TIME"); "TIME");
var safetyStatus = var safetyStatus =
new StatusLightData(Color.Red, Safety ? StatusLightState.On : StatusLightState.Off, "SAFETY"); new StatusLightData(Color.Red, Safety ? StatusLightState.On : StatusLightState.Off, "SAFETY");
wiresComponent.SetStatus(AirlockWireStatus.PowerIndicator, powerLight); wiresComponent.SetStatus(AirlockWireStatus.PowerIndicator, powerLight);
wiresComponent.SetStatus(AirlockWireStatus.BoltIndicator, boltStatus); wiresComponent.SetStatus(AirlockWireStatus.BoltIndicator, boltStatus);
wiresComponent.SetStatus(AirlockWireStatus.BoltLightIndicator, boltLightsStatus); wiresComponent.SetStatus(AirlockWireStatus.BoltLightIndicator, boltLightsStatus);
@@ -212,17 +212,6 @@ namespace Content.Server.Doors.Components
wiresComponent.IsWireCut(Wires.BackupPower); wiresComponent.IsWireCut(Wires.BackupPower);
} }
private void PowerDeviceOnOnPowerStateChanged(PowerChangedMessage e)
{
if (_entityManager.TryGetComponent<AppearanceComponent>(Owner, out var appearanceComponent))
{
appearanceComponent.SetData(DoorVisuals.Powered, e.Powered);
}
// BoltLights also got out
UpdateBoltLightStatus();
}
private enum Wires private enum Wires
{ {
/// <summary> /// <summary>
@@ -250,6 +239,7 @@ namespace Content.Server.Doors.Components
BoltLight, BoltLight,
// Placeholder for when AI is implemented // Placeholder for when AI is implemented
// aaaaany day now.
AIControl, AIControl,
/// <summary> /// <summary>
@@ -281,7 +271,7 @@ namespace Content.Server.Doors.Components
public void WiresUpdate(WiresUpdateEventArgs args) public void WiresUpdate(WiresUpdateEventArgs args)
{ {
if (!_entityManager.TryGetComponent<ServerDoorComponent>(Owner, out var doorComponent)) if (!_entityManager.TryGetComponent<DoorComponent>(Owner, out var doorComponent))
{ {
return; return;
} }
@@ -318,11 +308,13 @@ namespace Content.Server.Doors.Components
BoltLightsVisible = !_boltLightsWirePulsed; BoltLightsVisible = !_boltLightsWirePulsed;
break; break;
case Wires.Timing: case Wires.Timing:
// This is permanent, until the wire gets cut & mended.
AutoCloseDelayModifier = 0.5f; AutoCloseDelayModifier = 0.5f;
doorComponent.RefreshAutoClose(); EntitySystem.Get<AirlockSystem>().UpdateAutoClose(Owner, this);
break; break;
case Wires.Safety: case Wires.Safety:
Safety = !Safety; Safety = !Safety;
Dirty();
break; break;
} }
} }
@@ -341,11 +333,12 @@ namespace Content.Server.Doors.Components
BoltLightsVisible = true; BoltLightsVisible = true;
break; break;
case Wires.Timing: case Wires.Timing:
AutoClose = true; AutoCloseDelayModifier = 1;
doorComponent.RefreshAutoClose(); EntitySystem.Get<AirlockSystem>().UpdateAutoClose(Owner, this);
break; break;
case Wires.Safety: case Wires.Safety:
Safety = true; Safety = true;
Dirty();
break; break;
} }
} }
@@ -361,11 +354,12 @@ namespace Content.Server.Doors.Components
BoltLightsVisible = false; BoltLightsVisible = false;
break; break;
case Wires.Timing: case Wires.Timing:
AutoClose = false; AutoCloseDelayModifier = 0; // disable auto close
doorComponent.RefreshAutoClose(); EntitySystem.Get<AirlockSystem>().UpdateAutoClose(Owner, this);
break; break;
case Wires.Safety: case Wires.Safety:
Safety = false; Safety = false;
Dirty();
break; break;
} }
} }

View File

@@ -1,10 +0,0 @@
using Robust.Shared.GameObjects;
namespace Content.Server.Doors.Components
{
[RegisterComponent]
public class DoorBumpOpenerComponent : Component
{
public override string Name => "DoorBumpOpener";
}
}

View File

@@ -1,6 +1,7 @@
using Content.Server.Atmos.Components; using Content.Server.Atmos.Components;
using Content.Server.Atmos.EntitySystems; using Content.Server.Atmos.EntitySystems;
using Content.Shared.Doors; using Content.Server.Doors.Systems;
using Content.Shared.Doors.Components;
using Robust.Shared.GameObjects; using Robust.Shared.GameObjects;
using Robust.Shared.IoC; using Robust.Shared.IoC;
using Robust.Shared.Serialization.Manager.Attributes; using Robust.Shared.Serialization.Manager.Attributes;
@@ -27,9 +28,14 @@ namespace Content.Server.Doors.Components
public bool EmergencyPressureStop() public bool EmergencyPressureStop()
{ {
if (_entMan.TryGetComponent<ServerDoorComponent>(Owner, out var doorComp) && doorComp.State == SharedDoorComponent.DoorState.Open && doorComp.CanCloseGeneric()) var doorSys = EntitySystem.Get<DoorSystem>();
if (_entMan.TryGetComponent<DoorComponent>(Owner, out var door) &&
door.State == DoorState.Open &&
doorSys.CanClose(Owner, door))
{ {
doorComp.Close(); doorSys.StartClosing(Owner, door);
// Door system also sets airtight, but only after a delay. We want it to be immediate.
if (_entMan.TryGetComponent(Owner, out AirtightComponent? airtight)) if (_entMan.TryGetComponent(Owner, out AirtightComponent? airtight))
{ {
EntitySystem.Get<AirtightSystem>().SetAirblocked(airtight, true); EntitySystem.Get<AirtightSystem>().SetAirblocked(airtight, true);

View File

@@ -1,774 +0,0 @@
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Content.Server.Access;
using Content.Server.Atmos.Components;
using Content.Server.Atmos.EntitySystems;
using Content.Server.Construction;
using Content.Server.Construction.Components;
using Content.Server.Hands.Components;
using Content.Server.Stunnable;
using Content.Server.Tools;
using Content.Server.Tools.Components;
using Content.Shared.Access.Components;
using Content.Shared.Access.Systems;
using Content.Shared.Damage;
using Content.Shared.Doors;
using Content.Shared.Interaction;
using Content.Shared.Sound;
using Content.Shared.Tools;
using Robust.Shared.Audio;
using Robust.Shared.Containers;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Maths;
using Robust.Shared.Physics;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
using Robust.Shared.ViewVariables;
using Timer = Robust.Shared.Timing.Timer;
namespace Content.Server.Doors.Components
{
[RegisterComponent]
[ComponentReference(typeof(IActivate))]
[ComponentReference(typeof(SharedDoorComponent))]
public class ServerDoorComponent : SharedDoorComponent, IActivate, IInteractUsing, IMapInit
{
[Dependency] private readonly IEntityManager _entMan = default!;
[ViewVariables]
[DataField("board", customTypeSerializer:typeof(PrototypeIdSerializer<EntityPrototype>))]
private string? _boardPrototype;
[DataField("weldingQuality", customTypeSerializer:typeof(PrototypeIdSerializer<ToolQualityPrototype>))]
private string _weldingQuality = "Welding";
[DataField("pryingQuality", customTypeSerializer:typeof(PrototypeIdSerializer<ToolQualityPrototype>))]
private string _pryingQuality = "Prying";
[DataField("tryOpenDoorSound")]
private SoundSpecifier _tryOpenDoorSound = new SoundPathSpecifier("/Audio/Effects/bang.ogg");
[DataField("crushDamage", required: true)]
[ViewVariables(VVAccess.ReadWrite)]
public DamageSpecifier CrushDamage = default!;
[DataField("changeAirtight")]
public bool ChangeAirtight = true;
public override DoorState State
{
get => base.State;
protected set
{
if (State == value)
{
return;
}
base.State = value;
StateChangeStartTime = State switch
{
DoorState.Open or DoorState.Closed => null,
DoorState.Opening or DoorState.Closing => GameTiming.CurTime,
_ => throw new ArgumentOutOfRangeException(),
};
_entMan.EventBus.RaiseLocalEvent(Owner, new DoorStateChangedEvent(State), false);
_autoCloseCancelTokenSource?.Cancel();
Dirty();
}
}
private static readonly TimeSpan AutoCloseDelay = TimeSpan.FromSeconds(5);
private CancellationTokenSource? _stateChangeCancelTokenSource;
private CancellationTokenSource? _autoCloseCancelTokenSource;
private const float DoorStunTime = 5f;
/// <summary>
/// Whether the door will ever crush.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)] [DataField("inhibitCrush")]
private bool _inhibitCrush;
/// <summary>
/// Whether the door blocks light.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)] [DataField("occludes")]
private bool _occludes = true;
public bool Occludes => _occludes;
/// <summary>
/// Whether the door will open when it is bumped into.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)] [DataField("bumpOpen")]
public bool BumpOpen = true;
/// <summary>
/// Whether the door will open when it is activated or clicked.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)] [DataField("clickOpen")]
public bool ClickOpen = true;
/// <summary>
/// Whether the door starts open when it's first loaded from prototype. A door won't start open if its prototype is also welded shut.
/// Handled in Startup().
/// </summary>
[ViewVariables(VVAccess.ReadWrite)] [DataField("startOpen")]
private bool _startOpen = false;
/// <summary>
/// Whether the airlock is welded shut. Can be set by the prototype, although this will fail if the door isn't weldable.
/// When set by prototype, handled in Startup().
/// </summary>
[DataField("welded")]
private bool _isWeldedShut;
/// <summary>
/// Whether the airlock is welded shut.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
public bool IsWeldedShut
{
get => _isWeldedShut;
set
{
if (_isWeldedShut == value)
{
return;
}
_isWeldedShut = value;
SetAppearance(_isWeldedShut ? DoorVisualState.Welded : DoorVisualState.Closed);
}
}
/// <summary>
/// Whether the door can ever be welded shut.
/// </summary>
[DataField("weldable")]
private bool _weldable = true;
/// <summary>
/// Sound to play when the door opens.
/// </summary>
[DataField("openSound")]
public SoundSpecifier? OpenSound;
/// <summary>
/// Sound to play when the door closes.
/// </summary>
[DataField("closeSound")]
public SoundSpecifier? CloseSound;
/// <summary>
/// Sound to play if the door is denied.
/// </summary>
[DataField("denySound")]
public SoundSpecifier? DenySound;
/// <summary>
/// Should this door automatically close if its been open for too long?
/// </summary>
[DataField("autoClose")]
public bool AutoClose = true;
/// <summary>
/// Default time that the door should take to pry open.
/// </summary>
[DataField("pryTime")]
public float PryTime = 1.5f;
/// <summary>
/// Minimum interval allowed between deny sounds in milliseconds.
/// </summary>
[DataField("denySoundMinimumInterval")]
public float DenySoundMinimumInterval = 450.0f;
/// <summary>
/// Used to stop people from spamming the deny sound.
/// </summary>
private TimeSpan LastDenySoundTime = TimeSpan.Zero;
/// <summary>
/// Whether the door can currently be welded.
/// </summary>
private bool CanWeldShut => _weldable && State == DoorState.Closed;
/// <summary>
/// Whether something is currently using a welder on this so DoAfter isn't spammed.
/// </summary>
private bool _beingWelded;
//[ViewVariables(VVAccess.ReadWrite)]
//[DataField("canCrush")]
//private bool _canCrush = true; // TODO implement door crushing
protected override void Startup()
{
base.Startup();
if (IsWeldedShut)
{
if (!CanWeldShut)
{
Logger.Warning("{0} prototype loaded with incompatible flags: 'welded' is true, but door cannot be welded.", _entMan.GetComponent<MetaDataComponent>(Owner).EntityName);
return;
}
SetAppearance(DoorVisualState.Welded);
}
CreateDoorElectronicsBoard();
}
protected override void OnRemove()
{
_stateChangeCancelTokenSource?.Cancel();
_autoCloseCancelTokenSource?.Cancel();
base.OnRemove();
}
void IMapInit.MapInit()
{
if (_startOpen)
{
if (IsWeldedShut)
{
Logger.Warning("{0} prototype loaded with incompatible flags: 'welded' and 'startOpen' are both true.", _entMan.GetComponent<MetaDataComponent>(Owner).EntityName);
return;
}
QuickOpen(false);
}
CreateDoorElectronicsBoard();
}
void IActivate.Activate(ActivateEventArgs eventArgs)
{
if (!ClickOpen)
return;
var ev = new DoorClickShouldActivateEvent(eventArgs);
_entMan.EventBus.RaiseLocalEvent(Owner, ev, false);
if (ev.Handled)
return;
if (State == DoorState.Open)
{
TryClose(eventArgs.User);
}
else if (State == DoorState.Closed)
{
TryOpen(eventArgs.User);
}
}
#region Opening
public void TryOpen(EntityUid? user = null)
{
var msg = new DoorOpenAttemptEvent();
_entMan.EventBus.RaiseLocalEvent(Owner, msg);
if (msg.Cancelled) return;
if (user == null)
{
// a machine opened it or something, idk
Open();
}
else if (CanOpenByEntity(user.Value))
{
Open();
if (_entMan.TryGetComponent(user, out HandsComponent? hands) && hands.Count == 0)
{
SoundSystem.Play(Filter.Pvs(Owner), _tryOpenDoorSound.GetSound(), Owner,
AudioParams.Default.WithVolume(-2));
}
}
else
{
Deny();
}
}
public bool CanOpenByEntity(EntityUid user)
{
if (!CanOpenGeneric())
{
return false;
}
if (!_entMan.TryGetComponent(Owner, out AccessReaderComponent? access))
{
return true;
}
var doorSystem = EntitySystem.Get<DoorSystem>();
var isAirlockExternal = HasAccessType("External");
var accessSystem = EntitySystem.Get<AccessReaderSystem>();
return doorSystem.AccessType switch
{
DoorSystem.AccessTypes.AllowAll => true,
DoorSystem.AccessTypes.AllowAllIdExternal => isAirlockExternal || accessSystem.IsAllowed(access, user),
DoorSystem.AccessTypes.AllowAllNoExternal => !isAirlockExternal,
_ => accessSystem.IsAllowed(access, user)
};
}
/// <summary>
/// Returns whether a door has a certain access type. For example, maintenance doors will have access type
/// "Maintenance" in their AccessReader.
/// </summary>
private bool HasAccessType(string accessType)
{
if (_entMan.TryGetComponent(Owner, out AccessReaderComponent? access))
{
return access.AccessLists.Any(list => list.Contains(accessType));
}
return true;
}
/// <summary>
/// Checks if we can open at all, for anyone or anything. Will return false if inhibited by an IDoorCheck component.
/// </summary>
/// <returns>Boolean describing whether this door can open.</returns>
public bool CanOpenGeneric()
{
// note the welded check -- CanCloseGeneric does not have this
if (IsWeldedShut)
{
return false;
}
var ev = new BeforeDoorOpenedEvent();
_entMan.EventBus.RaiseLocalEvent(Owner, ev, false);
return !ev.Cancelled;
}
/// <summary>
/// Opens the door. Does not check if this is possible.
/// </summary>
public void Open()
{
State = DoorState.Opening;
if (Occludes && _entMan.TryGetComponent(Owner, out OccluderComponent? occluder))
{
occluder.Enabled = false;
}
if (ChangeAirtight && _entMan.TryGetComponent(Owner, out AirtightComponent? airtight))
{
EntitySystem.Get<AirtightSystem>().SetAirblocked(airtight, false);
}
_stateChangeCancelTokenSource?.Cancel();
_stateChangeCancelTokenSource = new();
if (OpenSound != null)
{
SoundSystem.Play(Filter.Pvs(Owner), OpenSound.GetSound(), Owner,
AudioParams.Default.WithVolume(-5));
}
Owner.SpawnTimer(OpenTimeOne, async () =>
{
OnPartialOpen();
await Timer.Delay(OpenTimeTwo, _stateChangeCancelTokenSource.Token);
State = DoorState.Open;
RefreshAutoClose();
}, _stateChangeCancelTokenSource.Token);
}
protected override void OnPartialOpen()
{
base.OnPartialOpen();
if (ChangeAirtight && _entMan.TryGetComponent(Owner, out AirtightComponent? airtight))
{
EntitySystem.Get<AirtightSystem>().SetAirblocked(airtight, false);
}
_entMan.EventBus.RaiseEvent(EventSource.Local, new AccessReaderChangeMessage(Owner, false));
}
private void QuickOpen(bool refresh)
{
if (Occludes && _entMan.TryGetComponent(Owner, out OccluderComponent? occluder))
{
occluder.Enabled = false;
}
OnPartialOpen();
State = DoorState.Open;
if(refresh)
RefreshAutoClose();
}
#endregion
#region Closing
public void TryClose(EntityUid? user = null)
{
var msg = new DoorCloseAttemptEvent();
_entMan.EventBus.RaiseLocalEvent(Owner, msg);
if (msg.Cancelled) return;
if (user != null && !CanCloseByEntity(user.Value))
{
Deny();
return;
}
Close();
}
public bool CanCloseByEntity(EntityUid user)
{
if (!CanCloseGeneric())
{
return false;
}
if (!_entMan.TryGetComponent(Owner, out AccessReaderComponent? access))
{
return true;
}
var accessSystem = EntitySystem.Get<AccessReaderSystem>();
return accessSystem.IsAllowed(access, user);
}
/// <summary>
/// Checks if we can close at all, for anyone or anything. Will return false if inhibited by an IDoorCheck component or if we are colliding with somebody while our Safety is on.
/// </summary>
/// <returns>Boolean describing whether this door can close.</returns>
public bool CanCloseGeneric()
{
var ev = new BeforeDoorClosedEvent();
_entMan.EventBus.RaiseLocalEvent(Owner, ev, false);
if (ev.Cancelled)
return false;
return !IsSafetyColliding();
}
private bool SafetyCheck()
{
var ev = new DoorSafetyEnabledEvent();
_entMan.EventBus.RaiseLocalEvent(Owner, ev, false);
return ev.Safety || _inhibitCrush;
}
/// <summary>
/// Checks if we care about safety, and if so, if something is colliding with it; ignores the CanCollide of the door's PhysicsComponent.
/// </summary>
/// <returns>True if something is colliding with us and we shouldn't crush things, false otherwise.</returns>
private bool IsSafetyColliding()
{
var safety = SafetyCheck();
if (safety && _entMan.TryGetComponent(Owner, out PhysicsComponent? physicsComponent))
{
var broadPhaseSystem = EntitySystem.Get<SharedPhysicsSystem>();
// Use this version so we can ignore the CanCollide being false
foreach(var _ in broadPhaseSystem.GetCollidingEntities(physicsComponent, -0.015f))
{
return true;
}
}
return false;
}
/// <summary>
/// Closes the door. Does not check if this is possible.
/// </summary>
public void Close()
{
State = DoorState.Closing;
// no more autoclose; we ARE closed
_autoCloseCancelTokenSource?.Cancel();
_stateChangeCancelTokenSource?.Cancel();
_stateChangeCancelTokenSource = new();
if (CloseSound != null)
{
SoundSystem.Play(Filter.Pvs(Owner), CloseSound.GetSound(), Owner,
AudioParams.Default.WithVolume(-5));
}
Owner.SpawnTimer(CloseTimeOne, async () =>
{
// if somebody walked into the door as it was closing, and we don't crush things
if (IsSafetyColliding())
{
Open();
return;
}
OnPartialClose();
await Timer.Delay(CloseTimeTwo, _stateChangeCancelTokenSource.Token);
if (Occludes && _entMan.TryGetComponent(Owner, out OccluderComponent? occluder))
{
occluder.Enabled = true;
}
State = DoorState.Closed;
}, _stateChangeCancelTokenSource.Token);
}
protected override void OnPartialClose()
{
base.OnPartialClose();
// if safety is off, crushes people inside of the door, temporarily turning off collisions with them while doing so.
var becomeairtight = ChangeAirtight && (SafetyCheck() || !TryCrush());
if (becomeairtight && _entMan.TryGetComponent(Owner, out AirtightComponent? airtight))
{
EntitySystem.Get<AirtightSystem>().SetAirblocked(airtight, true);
}
_entMan.EventBus.RaiseEvent(EventSource.Local, new AccessReaderChangeMessage(Owner, true));
}
/// <summary>
/// Crushes everyone colliding with us by more than 10%.
/// </summary>
/// <returns>True if we crushed somebody, false if we did not.</returns>
private bool TryCrush()
{
if (!_entMan.TryGetComponent(Owner, out PhysicsComponent physicsComponent) || physicsComponent is not IPhysBody body)
{
return false;
}
var collidingentities = body.GetCollidingEntities(Vector2.Zero, false);
if (!collidingentities.Any())
{
return false;
}
var doorAABB = body.GetWorldAABB();
var hitsomebody = false;
// Crush
foreach (var e in collidingentities)
{
var percentage = e.GetWorldAABB().IntersectPercentage(doorAABB);
if (percentage < 0.1f)
continue;
hitsomebody = true;
CurrentlyCrushing.Add(e.Owner);
if (_entMan.HasComponent<DamageableComponent>(e.Owner))
EntitySystem.Get<DamageableSystem>().TryChangeDamage(e.Owner, CrushDamage);
EntitySystem.Get<StunSystem>().TryParalyze(e.Owner, TimeSpan.FromSeconds(DoorStunTime), true);
}
// If we hit someone, open up after stun (opens right when stun ends)
if (hitsomebody)
{
Owner.SpawnTimer(TimeSpan.FromSeconds(DoorStunTime) - OpenTimeOne - OpenTimeTwo, Open);
return true;
}
return false;
}
#endregion
public void Deny()
{
var ev = new BeforeDoorDeniedEvent();
_entMan.EventBus.RaiseLocalEvent(Owner, ev, false);
if (ev.Cancelled)
return;
if (State == DoorState.Open || IsWeldedShut)
return;
if (DenySound != null)
{
if (LastDenySoundTime == TimeSpan.Zero)
{
LastDenySoundTime = _gameTiming.CurTime;
}
else
{
var difference = _gameTiming.CurTime - LastDenySoundTime;
if (difference < TimeSpan.FromMilliseconds(DenySoundMinimumInterval))
return;
}
LastDenySoundTime = _gameTiming.CurTime;
SoundSystem.Play(Filter.Pvs(Owner), DenySound.GetSound(), Owner,
AudioParams.Default.WithVolume(-3));
}
_stateChangeCancelTokenSource?.Cancel();
_stateChangeCancelTokenSource = new();
SetAppearance(DoorVisualState.Deny);
Owner.SpawnTimer(DenyTime, () =>
{
SetAppearance(DoorVisualState.Closed);
}, _stateChangeCancelTokenSource.Token);
}
/// <summary>
/// Starts a new auto close timer if this is appropriate
/// (i.e. event raised is not cancelled).
/// </summary>
public void RefreshAutoClose()
{
if (State != DoorState.Open)
return;
if (!AutoClose)
return;
var autoev = new BeforeDoorAutoCloseEvent();
_entMan.EventBus.RaiseLocalEvent(Owner, autoev, false);
if (autoev.Cancelled)
return;
_autoCloseCancelTokenSource = new();
var ev = new DoorGetCloseTimeModifierEvent();
_entMan.EventBus.RaiseLocalEvent(Owner, ev, false);
var realCloseTime = AutoCloseDelay * ev.CloseTimeModifier;
Owner.SpawnRepeatingTimer(realCloseTime, async () =>
{
if (CanCloseGeneric())
{
// Close() cancels _autoCloseCancellationTokenSource, so we're fine.
Close();
}
}, _autoCloseCancelTokenSource.Token);
}
async Task<bool> IInteractUsing.InteractUsing(InteractUsingEventArgs eventArgs)
{
if(!_entMan.TryGetComponent(eventArgs.Using, out ToolComponent? tool))
{
return false;
}
var toolSystem = EntitySystem.Get<ToolSystem>();
// for prying doors
if (tool.Qualities.Contains(_pryingQuality) && !IsWeldedShut)
{
var ev = new DoorGetPryTimeModifierEvent();
_entMan.EventBus.RaiseLocalEvent(Owner, ev, false);
var canEv = new BeforeDoorPryEvent(eventArgs);
_entMan.EventBus.RaiseLocalEvent(Owner, canEv, false);
if (canEv.Cancelled) return false;
var successfulPry = await toolSystem.UseTool(eventArgs.Using, eventArgs.User, Owner,
0f, ev.PryTimeModifier * PryTime, _pryingQuality);
if (successfulPry && !IsWeldedShut)
{
_entMan.EventBus.RaiseLocalEvent(Owner, new OnDoorPryEvent(eventArgs), false);
if (State == DoorState.Closed)
{
Open();
}
else if (State == DoorState.Open)
{
Close();
}
}
// regardless of whether this action actually succeeded, it generated a do-after bar. So prevent other
// interactions from happening afterwards by returning true.
return true;
}
// for welding doors
if (_beingWelded || !CanWeldShut || !_entMan.TryGetComponent(tool.Owner, out WelderComponent? welder) || !welder.Lit)
{
// no interaction occurred
return false;
}
_beingWelded = true;
// perform a do-after delay
var result = await toolSystem.UseTool(eventArgs.Using, eventArgs.User, Owner, 3f, 3f, _weldingQuality, () => CanWeldShut);
// if successful, toggle the weld-status (while also ensuring that it can still be welded shut after the delay)
if (result)
IsWeldedShut = CanWeldShut && !IsWeldedShut;
_beingWelded = false;
return true;
}
/// <summary>
/// Creates the corresponding door electronics board on the door.
/// This exists so when you deconstruct doors that were serialized with the map,
/// you can retrieve the door electronics board.
/// </summary>
private void CreateDoorElectronicsBoard()
{
// Ensure that the construction component is aware of the board container.
if (_entMan.TryGetComponent(Owner, out ConstructionComponent? construction))
EntitySystem.Get<ConstructionSystem>().AddContainer(Owner, "board", construction);
// We don't do anything if this is null or empty.
if (string.IsNullOrEmpty(_boardPrototype))
return;
var container = Owner.EnsureContainer<Container>("board", out var existed);
return;
/* // TODO ShadowCommander: Re-enable when access is added to boards. Requires map update.
if (existed)
{
// We already contain a board. Note: We don't check if it's the right one!
if (container.ContainedEntities.Count != 0)
return;
}
var board = Owner.EntityManager.SpawnEntity(_boardPrototype, Owner.Transform.Coordinates);
if(!container.Insert(board))
Logger.Warning($"Couldn't insert board {board} into door {Owner}!");
*/
}
public override ComponentState GetComponentState()
{
return new DoorComponentState(State, StateChangeStartTime, CurrentlyCrushing, GameTiming.CurTime);
}
}
}

View File

@@ -1,16 +1,19 @@
using Content.Server.Doors.Components; using Content.Server.Doors.Components;
using Content.Server.Power.Components; using Content.Server.Power.Components;
using Content.Server.WireHacking; using Content.Server.WireHacking;
using Content.Shared.Doors; using Content.Shared.Doors;
using Content.Shared.Doors.Components;
using Content.Shared.Doors.Systems;
using Content.Shared.Interaction;
using Content.Shared.Popups; using Content.Shared.Popups;
using Robust.Server.GameObjects; using Robust.Server.GameObjects;
using Robust.Shared.GameObjects; using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Localization; using Robust.Shared.Localization;
using System;
namespace Content.Server.Doors.Systems namespace Content.Server.Doors.Systems
{ {
public class AirlockSystem : EntitySystem public sealed class AirlockSystem : SharedAirlockSystem
{ {
public override void Initialize() public override void Initialize()
{ {
@@ -19,12 +22,8 @@ namespace Content.Server.Doors.Systems
SubscribeLocalEvent<AirlockComponent, PowerChangedEvent>(OnPowerChanged); SubscribeLocalEvent<AirlockComponent, PowerChangedEvent>(OnPowerChanged);
SubscribeLocalEvent<AirlockComponent, DoorStateChangedEvent>(OnStateChanged); SubscribeLocalEvent<AirlockComponent, DoorStateChangedEvent>(OnStateChanged);
SubscribeLocalEvent<AirlockComponent, BeforeDoorOpenedEvent>(OnBeforeDoorOpened); SubscribeLocalEvent<AirlockComponent, BeforeDoorOpenedEvent>(OnBeforeDoorOpened);
SubscribeLocalEvent<AirlockComponent, BeforeDoorClosedEvent>(OnBeforeDoorClosed);
SubscribeLocalEvent<AirlockComponent, BeforeDoorDeniedEvent>(OnBeforeDoorDenied); SubscribeLocalEvent<AirlockComponent, BeforeDoorDeniedEvent>(OnBeforeDoorDenied);
SubscribeLocalEvent<AirlockComponent, DoorSafetyEnabledEvent>(OnDoorSafetyCheck); SubscribeLocalEvent<AirlockComponent, ActivateInWorldEvent>(OnActivate, before: new [] {typeof(DoorSystem)});
SubscribeLocalEvent<AirlockComponent, BeforeDoorAutoCloseEvent>(OnDoorAutoCloseCheck);
SubscribeLocalEvent<AirlockComponent, DoorGetCloseTimeModifierEvent>(OnDoorCloseTimeModifier);
SubscribeLocalEvent<AirlockComponent, DoorClickShouldActivateEvent>(OnDoorClickShouldActivate);
SubscribeLocalEvent<AirlockComponent, BeforeDoorPryEvent>(OnDoorPry); SubscribeLocalEvent<AirlockComponent, BeforeDoorPryEvent>(OnDoorPry);
} }
@@ -35,21 +34,59 @@ namespace Content.Server.Doors.Systems
appearanceComponent.SetData(DoorVisuals.Powered, args.Powered); appearanceComponent.SetData(DoorVisuals.Powered, args.Powered);
} }
if (!args.Powered)
{
// stop any scheduled auto-closing
DoorSystem.SetNextStateChange(uid, null);
}
else
{
// door received power. Lets "wake" the door up, in case it is currently open and needs to auto-close.
DoorSystem.SetNextStateChange(uid, TimeSpan.FromSeconds(1));
}
// BoltLights also got out // BoltLights also got out
component.UpdateBoltLightStatus(); component.UpdateBoltLightStatus();
} }
private void OnStateChanged(EntityUid uid, AirlockComponent component, DoorStateChangedEvent args) private void OnStateChanged(EntityUid uid, AirlockComponent component, DoorStateChangedEvent args)
{ {
// TODO move to shared? having this be server-side, but having client-side door opening/closing & prediction
// means that sometimes the panels & bolt lights may be visible despite a door being completely open.
// Only show the maintenance panel if the airlock is closed // Only show the maintenance panel if the airlock is closed
if (TryComp<WiresComponent>(uid, out var wiresComponent)) if (TryComp<WiresComponent>(uid, out var wiresComponent))
{ {
wiresComponent.IsPanelVisible = wiresComponent.IsPanelVisible =
component.OpenPanelVisible component.OpenPanelVisible
|| args.State != SharedDoorComponent.DoorState.Open; || args.State != DoorState.Open;
} }
// If the door is closed, we should look if the bolt was locked while closing // If the door is closed, we should look if the bolt was locked while closing
component.UpdateBoltLightStatus(); component.UpdateBoltLightStatus();
UpdateAutoClose(uid, component);
}
/// <summary>
/// Updates the auto close timer.
/// </summary>
public void UpdateAutoClose(EntityUid uid, AirlockComponent? airlock = null, DoorComponent? door = null)
{
if (!Resolve(uid, ref airlock, ref door))
return;
if (door.State != DoorState.Open)
return;
if (!airlock.CanChangeState())
return;
var autoev = new BeforeDoorAutoCloseEvent();
RaiseLocalEvent(uid, autoev, false);
if (autoev.Cancelled)
return;
DoorSystem.SetNextStateChange(uid, airlock.AutoCloseDelay * airlock.AutoCloseDelayModifier);
} }
private void OnBeforeDoorOpened(EntityUid uid, AirlockComponent component, BeforeDoorOpenedEvent args) private void OnBeforeDoorOpened(EntityUid uid, AirlockComponent component, BeforeDoorOpenedEvent args)
@@ -58,11 +95,24 @@ namespace Content.Server.Doors.Systems
args.Cancel(); args.Cancel();
} }
private void OnBeforeDoorClosed(EntityUid uid, AirlockComponent component, BeforeDoorClosedEvent args) protected override void OnBeforeDoorClosed(EntityUid uid, SharedAirlockComponent component, BeforeDoorClosedEvent args)
{
base.OnBeforeDoorClosed(uid, component, args);
if (args.Cancelled)
return;
// only block based on bolts / power status when initially closing the door, not when its already
// mid-transition. Particularly relevant for when the door was pried-closed with a crowbar, which bypasses
// the initial power-check.
if (TryComp(uid, out DoorComponent? door)
&& !door.Partial
&& !Comp<AirlockComponent>(uid).CanChangeState())
{ {
if (!component.CanChangeState())
args.Cancel(); args.Cancel();
} }
}
private void OnBeforeDoorDenied(EntityUid uid, AirlockComponent component, BeforeDoorDeniedEvent args) private void OnBeforeDoorDenied(EntityUid uid, AirlockComponent component, BeforeDoorDeniedEvent args)
{ {
@@ -70,26 +120,10 @@ namespace Content.Server.Doors.Systems
args.Cancel(); args.Cancel();
} }
private void OnDoorSafetyCheck(EntityUid uid, AirlockComponent component, DoorSafetyEnabledEvent args) private void OnActivate(EntityUid uid, AirlockComponent component, ActivateInWorldEvent args)
{
args.Safety = component.Safety;
}
private void OnDoorAutoCloseCheck(EntityUid uid, AirlockComponent component, BeforeDoorAutoCloseEvent args)
{
if (!component.AutoClose)
args.Cancel();
}
private void OnDoorCloseTimeModifier(EntityUid uid, AirlockComponent component, DoorGetCloseTimeModifierEvent args)
{
args.CloseTimeModifier *= component.AutoCloseDelayModifier;
}
private void OnDoorClickShouldActivate(EntityUid uid, AirlockComponent component, DoorClickShouldActivateEvent args)
{ {
if (TryComp<WiresComponent>(uid, out var wiresComponent) && wiresComponent.IsPanelOpen && if (TryComp<WiresComponent>(uid, out var wiresComponent) && wiresComponent.IsPanelOpen &&
EntityManager.TryGetComponent(args.Args.User, out ActorComponent? actor)) EntityManager.TryGetComponent(args.User, out ActorComponent? actor))
{ {
wiresComponent.OpenInterface(actor.PlayerSession); wiresComponent.OpenInterface(actor.PlayerSession);
args.Handled = true; args.Handled = true;
@@ -100,12 +134,12 @@ namespace Content.Server.Doors.Systems
{ {
if (component.IsBolted()) if (component.IsBolted())
{ {
component.Owner.PopupMessage(args.Args.User, Loc.GetString("airlock-component-cannot-pry-is-bolted-message")); component.Owner.PopupMessage(args.User, Loc.GetString("airlock-component-cannot-pry-is-bolted-message"));
args.Cancel(); args.Cancel();
} }
if (component.IsPowered()) if (component.IsPowered())
{ {
component.Owner.PopupMessage(args.Args.User, Loc.GetString("airlock-component-cannot-pry-is-powered-message")); component.Owner.PopupMessage(args.User, Loc.GetString("airlock-component-cannot-pry-is-powered-message"));
args.Cancel(); args.Cancel();
} }
} }

View File

@@ -1,69 +1,293 @@
using Content.Server.Doors.Components; using Content.Server.Access;
using Content.Server.Atmos.Components;
using Content.Server.Atmos.EntitySystems;
using Content.Server.Construction;
using Content.Server.Construction.Components;
using Content.Server.Tools;
using Content.Server.Tools.Components;
using Content.Shared.Access.Components;
using Content.Shared.Access.Systems;
using Content.Shared.Doors; using Content.Shared.Doors;
using Content.Shared.Doors.Components;
using Content.Shared.Doors.Systems;
using Content.Shared.Interaction;
using Content.Shared.Tag;
using Robust.Shared.Audio;
using Robust.Shared.Containers;
using Robust.Shared.GameObjects; using Robust.Shared.GameObjects;
using Robust.Shared.IoC; using Robust.Shared.IoC;
using Robust.Shared.Physics.Dynamics; using Robust.Shared.Physics.Dynamics;
using Robust.Shared.Player;
using System.Linq;
namespace Content.Server.Doors namespace Content.Server.Doors.Systems;
{
/// <summary>
/// Used on the server side to manage global access level overrides.
/// </summary>
internal sealed class DoorSystem : SharedDoorSystem
{
/// <summary>
/// Determines the base access behavior of all doors on the station.
/// </summary>
public AccessTypes AccessType { get; set; }
/// <summary> public sealed class DoorSystem : SharedDoorSystem
/// How door access should be handled.
/// </summary>
public enum AccessTypes
{ {
/// <summary> ID based door access. </summary> [Dependency] private readonly ConstructionSystem _constructionSystem = default!;
Id, [Dependency] private readonly ToolSystem _toolSystem = default!;
/// <summary> [Dependency] private readonly AirtightSystem _airtightSystem = default!;
/// Allows everyone to open doors, except external which airlocks are still handled with ID's [Dependency] private readonly AccessReaderSystem _accessReaderSystem = default!;
/// </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() public override void Initialize()
{ {
base.Initialize(); base.Initialize();
AccessType = AccessTypes.Id; SubscribeLocalEvent<DoorComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<ServerDoorComponent, StartCollideEvent>(HandleCollide); SubscribeLocalEvent<DoorComponent, InteractUsingEvent>(OnInteractUsing);
SubscribeLocalEvent<DoorComponent, PryFinishedEvent>(OnPryFinished);
SubscribeLocalEvent<DoorComponent, PryCancelledEvent>(OnPryCancelled);
SubscribeLocalEvent<DoorComponent, WeldFinishedEvent>(OnWeldFinished);
SubscribeLocalEvent<DoorComponent, WeldCancelledEvent>(OnWeldCancelled);
} }
private void HandleCollide(EntityUid uid, ServerDoorComponent component, StartCollideEvent args) // TODO AUDIO PREDICT Figure out a better way to handle sound and prediction. For now, this works well enough?
//
// Currently a client will predict when a door is going to close automatically. So any client in PVS range can just
// play their audio locally. Playing it server-side causes an odd delay, while in shared it causes double-audio.
//
// But if we just do that, then if a door is closed prematurely as the result of an interaction (i.e., using "E" on
// an open door), then the audio would only be played for the client performing the interaction.
//
// So we do this:
// - Play audio client-side IF the closing is being predicted (auto-close or predicted interaction)
// - Server assumes automated closing is predicted by clients and does not play audio unless otherwise specified.
// - Major exception is player interactions, which other players cannot predict
// - In that case, send audio to all players, except possibly the interacting player if it was a predicted
// interaction.
/// <summary>
/// Selectively send sound to clients, taking care to not send the double-audio.
/// </summary>
/// <param name="uid">The audio source</param>
/// <param name="sound">The sound</param>
/// <param name="predictingPlayer">The user (if any) that instigated an interaction</param>
/// <param name="predicted">Whether this interaction would have been predicted. If the predicting player is null,
/// this assumes it would have been predicted by all players in PVS range.</param>
protected override void PlaySound(EntityUid uid, string sound, AudioParams audioParams, EntityUid? predictingPlayer, bool predicted)
{ {
if (!EntityManager.HasComponent<DoorBumpOpenerComponent>(args.OtherFixture.Body.Owner)) // If this sound would have been predicted by all clients, do not play any audio.
if (predicted && predictingPlayer == null)
return;
var filter = Filter.Pvs(uid);
if (predicted)
{ {
// This interaction is predicted, but only by the instigating user, who will have played their own sounds.
filter.RemoveWhereAttachedEntity(e => e == predictingPlayer);
}
// send the sound to players.
SoundSystem.Play(filter, sound, uid, AudioParams.Default.WithVolume(-5));
}
#region DoAfters
/// <summary>
/// Weld or pry open a door.
/// </summary>
private void OnInteractUsing(EntityUid uid, DoorComponent door, InteractUsingEvent args)
{
if (args.Handled)
return;
if (!TryComp(args.Used, out ToolComponent? tool))
return;
if (tool.Qualities.Contains(door.PryingQuality))
{
TryPryDoor(uid, args.Used, args.User, door);
args.Handled = true;
return; return;
} }
if (component.State != SharedDoorComponent.DoorState.Closed) if (door.Weldable && tool.Qualities.Contains(door.WeldingQuality))
{ {
TryWeldDoor(uid, args.Used, args.User, door);
args.Handled = true;
}
}
/// <summary>
/// Attempt to weld a door shut, or unweld it if it is already welded. This does not actually check if the user
/// is holding the correct tool.
/// </summary>
private async void TryWeldDoor(EntityUid target, EntityUid used, EntityUid user, DoorComponent door)
{
if (!door.Weldable || door.BeingWelded || door.CurrentlyCrushing.Count > 0)
return;
// is the door in a weld-able state?
if (door.State != DoorState.Closed && door.State != DoorState.Welded)
return;
// perform a do-after delay
door.BeingWelded = true;
_toolSystem.UseTool(used, user, target, 3f, 3f, door.WeldingQuality,
new WeldFinishedEvent(), new WeldCancelledEvent(), target);
}
/// <summary>
/// Pry open a door. This does not check if the user is holding the required tool.
/// </summary>
private async void TryPryDoor(EntityUid target, EntityUid tool, EntityUid user, DoorComponent door)
{
if (door.State == DoorState.Welded)
return;
var canEv = new BeforeDoorPryEvent(user);
RaiseLocalEvent(target, canEv, false);
if (canEv.Cancelled)
return;
var modEv = new DoorGetPryTimeModifierEvent();
RaiseLocalEvent(target, modEv, false);
_toolSystem.UseTool(tool, user, target, 0f, modEv.PryTimeModifier * door.PryTime, door.PryingQuality,
new PryFinishedEvent(), new PryCancelledEvent(), target);
}
private void OnWeldCancelled(EntityUid uid, DoorComponent door, WeldCancelledEvent args)
{
door.BeingWelded = false;
}
private void OnWeldFinished(EntityUid uid, DoorComponent door, WeldFinishedEvent args)
{
door.BeingWelded = false;
if (!door.Weldable)
return;
if (door.State == DoorState.Closed)
SetState(uid, DoorState.Welded, door);
else if (door.State == DoorState.Welded)
SetState(uid, DoorState.Closed, door);
}
private void OnPryCancelled(EntityUid uid, DoorComponent door, PryCancelledEvent args)
{
door.BeingPried = false;
}
private void OnPryFinished(EntityUid uid, DoorComponent door, PryFinishedEvent args)
{
door.BeingPried = false;
if (door.State == DoorState.Closed)
StartOpening(uid, door);
else if (door.State == DoorState.Open)
StartClosing(uid, door);
}
#endregion
/// <summary>
/// Does the user have the permissions required to open this door?
/// </summary>
public override bool HasAccess(EntityUid uid, EntityUid? user = null, AccessReaderComponent? access = null)
{
// TODO network AccessComponent for predicting doors
// if there is no "user" we skip the access checks. Access is also ignored in some game-modes.
if (user == null || AccessType == AccessTypes.AllowAll)
return true;
if (!Resolve(uid, ref access, false))
return true;
var isExternal = access.AccessLists.Any(list => list.Contains("External"));
return AccessType switch
{
// Some game modes modify access rules.
AccessTypes.AllowAllIdExternal => !isExternal || _accessReaderSystem.IsAllowed(access, user.Value),
AccessTypes.AllowAllNoExternal => !isExternal,
_ => _accessReaderSystem.IsAllowed(access, user.Value)
};
}
/// <summary>
/// Open a door if a player or door-bumper (PDA, ID-card) collide with the door. Sadly, bullets no longer
/// generate "access denied" sounds as you fire at a door.
/// </summary>
protected override void HandleCollide(EntityUid uid, DoorComponent door, StartCollideEvent args)
{
// TODO ACCESS READER move access reader to shared and predict door opening/closing
// Then this can be moved to the shared system without mispredicting.
if (!door.BumpOpen)
return;
if (door.State != DoorState.Closed)
return;
if (TryComp(args.OtherFixture.Body.Owner, out TagComponent? tags) && tags.HasTag("DoorBumpOpener"))
TryOpen(uid, door, args.OtherFixture.Body.Owner);
}
public override void OnPartialOpen(EntityUid uid, DoorComponent? door = null, PhysicsComponent? physics = null)
{
if (!Resolve(uid, ref door, ref physics))
return;
base.OnPartialOpen(uid, door, physics);
if (door.ChangeAirtight && TryComp(uid, out AirtightComponent? airtight))
{
_airtightSystem.SetAirblocked(airtight, false);
}
// Path-finding. Has nothing directly to do with access readers.
RaiseLocalEvent(new AccessReaderChangeMessage(uid, false));
}
public override bool OnPartialClose(EntityUid uid, DoorComponent? door = null, PhysicsComponent? physics = null)
{
if (!Resolve(uid, ref door, ref physics))
return false;
if (!base.OnPartialClose(uid, door, physics))
return false;
// update airtight, if we did not crush something.
if (door.ChangeAirtight && door.CurrentlyCrushing.Count == 0 && TryComp(uid, out AirtightComponent? airtight))
_airtightSystem.SetAirblocked(airtight, true);
// Path-finding. Has nothing directly to do with access readers.
RaiseLocalEvent(new AccessReaderChangeMessage(uid, true));
return true;
}
private void OnMapInit(EntityUid uid, DoorComponent door, MapInitEvent args)
{
// Ensure that the construction component is aware of the board container.
if (TryComp(uid, out ConstructionComponent? construction))
_constructionSystem.AddContainer(uid, "board", construction);
// We don't do anything if this is null or empty.
if (string.IsNullOrEmpty(door.BoardPrototype))
return;
var container = uid.EnsureContainer<Container>("board", out var existed);
/* // TODO ShadowCommander: Re-enable when access is added to boards. Requires map update.
if (existed)
{
// We already contain a board. Note: We don't check if it's the right one!
if (container.ContainedEntities.Count != 0)
return; return;
} }
if (!component.BumpOpen) var board = Owner.EntityManager.SpawnEntity(_boardPrototype, Owner.Transform.Coordinates);
{
return; if(!container.Insert(board))
Logger.Warning($"Couldn't insert board {board} into door {Owner}!");
*/
}
} }
// Disabled because it makes it suck hard to walk through double doors. public class PryFinishedEvent : EntityEventArgs { }
public class PryCancelledEvent : EntityEventArgs { }
component.TryOpen(args.OtherFixture.Body.Owner); public class WeldFinishedEvent : EntityEventArgs { }
} public class WeldCancelledEvent : EntityEventArgs { }
}
}

View File

@@ -1,17 +1,21 @@
using Content.Server.Atmos.Monitor.Components; using Content.Server.Atmos.Monitor.Components;
using Content.Server.Atmos.Monitor.Systems; using Content.Server.Atmos.Monitor.Systems;
using Content.Server.Doors.Components; using Content.Server.Doors.Components;
using Content.Shared.Atmos.Monitor; using Content.Shared.Atmos.Monitor;
using Content.Shared.Doors; using Content.Shared.Doors;
using Content.Shared.Doors.Components;
using Content.Shared.Doors.Systems;
using Content.Shared.Popups; using Content.Shared.Popups;
using Robust.Shared.GameObjects; using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Localization; using Robust.Shared.Localization;
using Robust.Shared.Log;
namespace Content.Server.Doors.Systems namespace Content.Server.Doors.Systems
{ {
public class FirelockSystem : EntitySystem public class FirelockSystem : EntitySystem
{ {
[Dependency] private readonly SharedDoorSystem _doorSystem = default!;
public override void Initialize() public override void Initialize()
{ {
base.Initialize(); base.Initialize();
@@ -19,8 +23,8 @@ namespace Content.Server.Doors.Systems
SubscribeLocalEvent<FirelockComponent, BeforeDoorOpenedEvent>(OnBeforeDoorOpened); SubscribeLocalEvent<FirelockComponent, BeforeDoorOpenedEvent>(OnBeforeDoorOpened);
SubscribeLocalEvent<FirelockComponent, BeforeDoorDeniedEvent>(OnBeforeDoorDenied); SubscribeLocalEvent<FirelockComponent, BeforeDoorDeniedEvent>(OnBeforeDoorDenied);
SubscribeLocalEvent<FirelockComponent, DoorGetPryTimeModifierEvent>(OnDoorGetPryTimeModifier); SubscribeLocalEvent<FirelockComponent, DoorGetPryTimeModifierEvent>(OnDoorGetPryTimeModifier);
SubscribeLocalEvent<FirelockComponent, DoorClickShouldActivateEvent>(OnDoorClickShouldActivate);
SubscribeLocalEvent<FirelockComponent, BeforeDoorPryEvent>(OnBeforeDoorPry); SubscribeLocalEvent<FirelockComponent, BeforeDoorPryEvent>(OnBeforeDoorPry);
SubscribeLocalEvent<FirelockComponent, BeforeDoorAutoCloseEvent>(OnBeforeDoorAutoclose); SubscribeLocalEvent<FirelockComponent, BeforeDoorAutoCloseEvent>(OnBeforeDoorAutoclose);
SubscribeLocalEvent<FirelockComponent, AtmosMonitorAlarmEvent>(OnAtmosAlarm); SubscribeLocalEvent<FirelockComponent, AtmosMonitorAlarmEvent>(OnAtmosAlarm);
} }
@@ -42,26 +46,20 @@ namespace Content.Server.Doors.Systems
args.PryTimeModifier *= component.LockedPryTimeModifier; args.PryTimeModifier *= component.LockedPryTimeModifier;
} }
private void OnDoorClickShouldActivate(EntityUid uid, FirelockComponent component, DoorClickShouldActivateEvent args)
{
// We're a firelock, you can't click to open it
args.Handled = true;
}
private void OnBeforeDoorPry(EntityUid uid, FirelockComponent component, BeforeDoorPryEvent args) private void OnBeforeDoorPry(EntityUid uid, FirelockComponent component, BeforeDoorPryEvent args)
{ {
if (!TryComp<ServerDoorComponent>(uid, out var doorComponent) || doorComponent.State != SharedDoorComponent.DoorState.Closed) if (!TryComp<DoorComponent>(uid, out var door) || door.State != DoorState.Closed)
{ {
return; return;
} }
if (component.IsHoldingPressure()) if (component.IsHoldingPressure())
{ {
component.Owner.PopupMessage(args.Args.User, Loc.GetString("firelock-component-is-holding-pressure-message")); component.Owner.PopupMessage(args.User, Loc.GetString("firelock-component-is-holding-pressure-message"));
} }
else if (component.IsHoldingFire()) else if (component.IsHoldingFire())
{ {
component.Owner.PopupMessage(args.Args.User, Loc.GetString("firelock-component-is-holding-fire-message")); component.Owner.PopupMessage(args.User, Loc.GetString("firelock-component-is-holding-fire-message"));
} }
} }
@@ -81,12 +79,12 @@ namespace Content.Server.Doors.Systems
private void OnAtmosAlarm(EntityUid uid, FirelockComponent component, AtmosMonitorAlarmEvent args) private void OnAtmosAlarm(EntityUid uid, FirelockComponent component, AtmosMonitorAlarmEvent args)
{ {
if (!TryComp<ServerDoorComponent>(uid, out var doorComponent)) return; if (!TryComp<DoorComponent>(uid, out var doorComponent)) return;
if (args.HighestNetworkType == AtmosMonitorAlarmType.Normal) if (args.HighestNetworkType == AtmosMonitorAlarmType.Normal)
{ {
if (doorComponent.State == SharedDoorComponent.DoorState.Closed) if (doorComponent.State == DoorState.Closed)
doorComponent.Open(); _doorSystem.TryOpen(uid);
} }
else if (args.HighestNetworkType == AtmosMonitorAlarmType.Danger) else if (args.HighestNetworkType == AtmosMonitorAlarmType.Danger)
{ {

View File

@@ -1,6 +1,8 @@
using System; using System;
using Content.Server.Administration.Logs; using Content.Server.Administration.Logs;
using Content.Server.Doors;
using Content.Server.Doors.Components; using Content.Server.Doors.Components;
using Content.Server.Doors.Systems;
using Content.Server.Explosion.Components; using Content.Server.Explosion.Components;
using Content.Server.Flash; using Content.Server.Flash;
using Content.Server.Flash.Components; using Content.Server.Flash.Components;
@@ -39,6 +41,7 @@ namespace Content.Server.Explosion.EntitySystems
{ {
[Dependency] private readonly ExplosionSystem _explosions = default!; [Dependency] private readonly ExplosionSystem _explosions = default!;
[Dependency] private readonly FlashSystem _flashSystem = default!; [Dependency] private readonly FlashSystem _flashSystem = default!;
[Dependency] private readonly DoorSystem _sharedDoorSystem = default!;
public override void Initialize() public override void Initialize()
{ {
@@ -106,21 +109,7 @@ namespace Content.Server.Explosion.EntitySystems
private void HandleDoorTrigger(EntityUid uid, ToggleDoorOnTriggerComponent component, TriggerEvent args) private void HandleDoorTrigger(EntityUid uid, ToggleDoorOnTriggerComponent component, TriggerEvent args)
{ {
if (EntityManager.TryGetComponent<ServerDoorComponent>(uid, out var door)) _sharedDoorSystem.TryToggleDoor(uid);
{
switch (door.State)
{
case SharedDoorComponent.DoorState.Open:
door.Close();
break;
case SharedDoorComponent.DoorState.Closed:
door.Open();
break;
case SharedDoorComponent.DoorState.Closing:
case SharedDoorComponent.DoorState.Opening:
break;
}
}
} }
private void HandleCollide(EntityUid uid, TriggerOnCollideComponent component, StartCollideEvent args) private void HandleCollide(EntityUid uid, TriggerOnCollideComponent component, StartCollideEvent args)

View File

@@ -3,7 +3,6 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using Content.Server.Chat.Managers; using Content.Server.Chat.Managers;
using Content.Server.Doors;
using Content.Server.Players; using Content.Server.Players;
using Content.Server.Roles; using Content.Server.Roles;
using Content.Server.Suspicion; using Content.Server.Suspicion;
@@ -11,6 +10,7 @@ using Content.Server.Suspicion.Roles;
using Content.Server.Traitor.Uplink; using Content.Server.Traitor.Uplink;
using Content.Server.Traitor.Uplink.Account; using Content.Server.Traitor.Uplink.Account;
using Content.Shared.CCVar; using Content.Shared.CCVar;
using Content.Shared.Doors.Systems;
using Content.Shared.GameTicking; using Content.Shared.GameTicking;
using Content.Shared.MobState.Components; using Content.Shared.MobState.Components;
using Content.Shared.Roles; using Content.Shared.Roles;
@@ -49,7 +49,7 @@ public sealed class SuspicionRuleSystem : GameRuleSystem
[Dependency] private readonly IEntityManager _entities = default!; [Dependency] private readonly IEntityManager _entities = default!;
[Dependency] private readonly IRobustRandom _random = default!; [Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!; [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly DoorSystem _doorSystem = default!; [Dependency] private readonly SharedDoorSystem _doorSystem = default!;
public override string Prototype => "Suspicion"; public override string Prototype => "Suspicion";
@@ -217,7 +217,7 @@ public sealed class SuspicionRuleSystem : GameRuleSystem
SoundSystem.Play(filter, _addedSound.GetSound(), AudioParams.Default); SoundSystem.Play(filter, _addedSound.GetSound(), AudioParams.Default);
_doorSystem.AccessType = DoorSystem.AccessTypes.AllowAllNoExternal; _doorSystem.AccessType = SharedDoorSystem.AccessTypes.AllowAllNoExternal;
_checkTimerCancel = new CancellationTokenSource(); _checkTimerCancel = new CancellationTokenSource();
Timer.SpawnRepeating(DeadCheckDelay, CheckWinConditions, _checkTimerCancel.Token); Timer.SpawnRepeating(DeadCheckDelay, CheckWinConditions, _checkTimerCancel.Token);
@@ -225,7 +225,7 @@ public sealed class SuspicionRuleSystem : GameRuleSystem
public override void Removed() public override void Removed()
{ {
_doorSystem.AccessType = DoorSystem.AccessTypes.Id; _doorSystem.AccessType = SharedDoorSystem.AccessTypes.Id;
EndTime = null; EndTime = null;
_traitors.Clear(); _traitors.Clear();

View File

@@ -1,8 +1,10 @@
using System; using System;
using Content.Server.Doors.Components; using Content.Server.Doors.Components;
using Content.Server.Doors.Systems;
using Content.Server.Power.Components; using Content.Server.Power.Components;
using Content.Server.Shuttles.Components; using Content.Server.Shuttles.Components;
using Content.Shared.Doors; using Content.Shared.Doors;
using Content.Shared.Doors.Components;
using Content.Shared.Verbs; using Content.Shared.Verbs;
using Robust.Shared.GameObjects; using Robust.Shared.GameObjects;
using Robust.Shared.IoC; using Robust.Shared.IoC;
@@ -23,6 +25,7 @@ namespace Content.Server.Shuttles.EntitySystems
[Dependency] private readonly IMapManager _mapManager = default!; [Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly FixtureSystem _fixtureSystem = default!; [Dependency] private readonly FixtureSystem _fixtureSystem = default!;
[Dependency] private readonly SharedJointSystem _jointSystem = default!; [Dependency] private readonly SharedJointSystem _jointSystem = default!;
[Dependency] private readonly DoorSystem _doorSystem = default!;
private const string DockingFixture = "docking"; private const string DockingFixture = "docking";
private const string DockingJoint = "docking"; private const string DockingJoint = "docking";
@@ -365,16 +368,16 @@ namespace Content.Server.Shuttles.EntitySystems
dockA.DockJoint = joint; dockA.DockJoint = joint;
dockB.DockJoint = joint; dockB.DockJoint = joint;
if (TryComp(dockA.Owner, out ServerDoorComponent? doorA)) if (TryComp(dockA.Owner, out DoorComponent? doorA))
{ {
doorA.ChangeAirtight = false; doorA.ChangeAirtight = false;
doorA.Open(); _doorSystem.StartOpening(doorA.Owner, doorA);
} }
if (TryComp(dockB.Owner, out ServerDoorComponent? doorB)) if (TryComp(dockB.Owner, out DoorComponent? doorB))
{ {
doorB.ChangeAirtight = false; doorB.ChangeAirtight = false;
doorB.Open(); _doorSystem.StartOpening(doorB.Owner, doorB);
} }
var msg = new DockEvent var msg = new DockEvent
@@ -448,16 +451,16 @@ namespace Content.Server.Shuttles.EntitySystems
return; return;
} }
if (TryComp(dock.Owner, out ServerDoorComponent? doorA)) if (TryComp(dock.Owner, out DoorComponent? doorA))
{ {
doorA.ChangeAirtight = true; doorA.ChangeAirtight = true;
doorA.Close(); _doorSystem.TryClose(doorA.Owner, doorA);
} }
if (TryComp(dock.DockedWith, out ServerDoorComponent? doorB)) if (TryComp(dock.DockedWith, out DoorComponent? doorB))
{ {
doorB.ChangeAirtight = true; doorB.ChangeAirtight = true;
doorB.Close(); _doorSystem.TryClose(doorB.Owner, doorB);
} }
// Could maybe give the shuttle a light push away, or at least if there's no other docks left? // Could maybe give the shuttle a light push away, or at least if there's no other docks left?

View File

@@ -0,0 +1,229 @@
using System;
using System.Collections.Generic;
using Content.Shared.Damage;
using Content.Shared.Sound;
using Content.Shared.Tools;
using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
using Robust.Shared.ViewVariables;
using DrawDepthTag = Robust.Shared.GameObjects.DrawDepth;
namespace Content.Shared.Doors.Components;
[NetworkedComponent]
[RegisterComponent]
public sealed class DoorComponent : Component
{
public override string Name => "Door";
/// <summary>
/// The current state of the door -- whether it is open, closed, opening, or closing.
/// </summary>
/// <remarks>
/// This should never be set directly.
/// </remarks>
[ViewVariables(VVAccess.ReadWrite)]
public DoorState State = DoorState.Closed;
[DataField("startOpen")]
public readonly bool StartOpen = false;
#region Timing
// if you want do dynamically adjust these times, you need to add networking for them. So for now, they are all
// read-only.
/// <summary>
/// Closing time until impassable. Total time is this plus <see cref="CloseTimeTwo"/>.
/// </summary>
[DataField("closeTimeOne")]
public readonly TimeSpan CloseTimeOne = TimeSpan.FromSeconds(0.4f);
/// <summary>
/// Closing time until fully closed. Total time is this plus <see cref="CloseTimeOne"/>.
/// </summary>
[DataField("closeTimeTwo")]
public readonly TimeSpan CloseTimeTwo = TimeSpan.FromSeconds(0.2f);
/// <summary>
/// Opening time until passable. Total time is this plus <see cref="OpenTimeTwo"/>.
/// </summary>
[DataField("openTimeOne")]
public readonly TimeSpan OpenTimeOne = TimeSpan.FromSeconds(0.4f);
/// <summary>
/// Opening time until fully open. Total time is this plus <see cref="OpenTimeOne"/>.
/// </summary>
[DataField("openTimeTwo")]
public readonly TimeSpan OpenTimeTwo = TimeSpan.FromSeconds(0.2f);
/// <summary>
/// Interval between deny sounds & visuals;
/// </summary>
[DataField("denyDuration")]
public readonly TimeSpan DenyDuration = TimeSpan.FromSeconds(0.45f);
/// <summary>
/// When the door is active, this is the time when the state will next update.
/// </summary>
public TimeSpan? NextStateChange;
/// <summary>
/// Whether the door is currently partially closed or open. I.e., when the door is "closing" and is already opaque,
/// but not yet actually closed.
/// </summary>
public bool Partial;
#endregion
#region Welding
// TODO WELDING. Consider creating a WeldableComponent for use with doors, crates and lockers? Currently they all
// have their own welding logic.
[DataField("weldingQuality", customTypeSerializer: typeof(PrototypeIdSerializer<ToolQualityPrototype>))]
public string WeldingQuality = "Welding";
/// <summary>
/// Whether the door can ever be welded shut.
/// </summary>
[DataField("weldable")]
public bool Weldable = true;
/// <summary>
/// Whether something is currently using a welder on this so DoAfter isn't spammed.
/// </summary>
public bool BeingWelded;
#endregion
public bool BeingPried;
#region Sounds
/// <summary>
/// Sound to play when the door opens.
/// </summary>
[DataField("openSound")]
public SoundSpecifier? OpenSound;
/// <summary>
/// Sound to play when the door closes.
/// </summary>
[DataField("closeSound")]
public SoundSpecifier? CloseSound;
/// <summary>
/// Sound to play if the door is denied.
/// </summary>
[DataField("denySound")]
public SoundSpecifier? DenySound;
/// <summary>
/// Sound to play when a disarmed (hands comp with 0 hands) entity opens the door. What?
/// </summary>
[DataField("tryOpenDoorSound")]
public SoundSpecifier TryOpenDoorSound = new SoundPathSpecifier("/Audio/Effects/bang.ogg");
#endregion
#region Crushing
/// <summary>
/// This is how long a door-crush will stun you. This also determines how long it takes the door to open up
/// again. Total stun time is actually given by this plus <see cref="OpenTimeOne"/>.
/// </summary>
[DataField("doorStunTime")]
public readonly TimeSpan DoorStunTime = TimeSpan.FromSeconds(2f);
[DataField("crushDamage")]
public DamageSpecifier? CrushDamage;
/// <summary>
/// If false, this door is incapable of crushing entities. Note that this differs from the airlock's "safety"
/// feature that checks for colliding entities.
/// </summary>
[DataField("canCrush")]
public readonly bool CanCrush = true;
/// <summary>
/// List of EntityUids of entities we're currently crushing. Cleared in OnPartialOpen().
/// </summary>
public List<EntityUid> CurrentlyCrushing = new();
#endregion
[DataField("board", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
public string? BoardPrototype;
[DataField("pryingQuality", customTypeSerializer: typeof(PrototypeIdSerializer<ToolQualityPrototype>))]
public string PryingQuality = "Prying";
/// <summary>
/// Default time that the door should take to pry open.
/// </summary>
[DataField("pryTime")]
public float PryTime = 1.5f;
[DataField("changeAirtight")]
public bool ChangeAirtight = true;
/// <summary>
/// Whether the door blocks light.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField("occludes")]
public bool Occludes = true;
/// <summary>
/// Whether the door will open when it is bumped into.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField("bumpOpen")]
public bool BumpOpen = true;
/// <summary>
/// Whether the door will open when it is activated or clicked.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField("clickOpen")]
public bool ClickOpen = true;
[DataField("openDrawDepth", customTypeSerializer: typeof(ConstantSerializer<DrawDepthTag>))]
public int OpenDrawDepth = (int) DrawDepth.DrawDepth.Doors;
[DataField("closedDrawDepth", customTypeSerializer: typeof(ConstantSerializer<DrawDepthTag>))]
public int ClosedDrawDepth = (int) DrawDepth.DrawDepth.Doors;
}
[Serializable, NetSerializable]
public enum DoorState
{
Closed,
Closing,
Open,
Opening,
Welded,
Denying,
}
[Serializable, NetSerializable]
public enum DoorVisuals
{
State,
Powered,
BoltLights
}
[Serializable, NetSerializable]
public class DoorComponentState : ComponentState
{
public readonly DoorState DoorState;
public readonly List<EntityUid> CurrentlyCrushing;
public readonly TimeSpan? NextStateChange;
public readonly bool Partial;
public DoorComponentState(DoorComponent door)
{
DoorState = door.State;
CurrentlyCrushing = door.CurrentlyCrushing;
NextStateChange = door.NextStateChange;
Partial = door.Partial;
}
}

View File

@@ -0,0 +1,29 @@
using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.ViewVariables;
using System;
namespace Content.Shared.Doors.Components;
[NetworkedComponent]
public abstract class SharedAirlockComponent : Component
{
public override string Name => "Airlock";
[ViewVariables(VVAccess.ReadWrite)]
[DataField("safety")]
public bool Safety = true;
}
[Serializable, NetSerializable]
public class AirlockComponentState : ComponentState
{
public readonly bool Safety;
public AirlockComponentState(bool safety)
{
Safety = safety;
}
}

View File

@@ -1,4 +1,4 @@
using Content.Shared.Interaction; using Content.Shared.Doors.Components;
using Robust.Shared.GameObjects; using Robust.Shared.GameObjects;
namespace Content.Shared.Doors namespace Content.Shared.Doors
@@ -8,9 +8,9 @@ namespace Content.Shared.Doors
/// </summary> /// </summary>
public class DoorStateChangedEvent : EntityEventArgs public class DoorStateChangedEvent : EntityEventArgs
{ {
public SharedDoorComponent.DoorState State; public readonly DoorState State;
public DoorStateChangedEvent(SharedDoorComponent.DoorState state) public DoorStateChangedEvent(DoorState state)
{ {
State = state; State = state;
} }
@@ -28,6 +28,11 @@ namespace Content.Shared.Doors
/// Raised when the door is determining whether it is able to close. /// Raised when the door is determining whether it is able to close.
/// Cancel to stop the door from being closed. /// Cancel to stop the door from being closed.
/// </summary> /// </summary>
/// <remarks>
/// This event is raised both when the door is initially closed, and when it is just about to become "partially"
/// closed (opaque & collidable). If canceled while partially closing, it will start opening again. Useful for
/// things like airlock anti-crush safety features.
/// </remarks>
public class BeforeDoorClosedEvent : CancellableEntityEventArgs public class BeforeDoorClosedEvent : CancellableEntityEventArgs
{ {
} }
@@ -40,19 +45,13 @@ namespace Content.Shared.Doors
{ {
} }
/// <summary>
/// Raised to determine whether the door's safety is on.
/// Modify Safety to set the door's safety.
/// </summary>
public class DoorSafetyEnabledEvent : HandledEntityEventArgs
{
public bool Safety = false;
}
/// <summary> /// <summary>
/// Raised to determine whether the door should automatically close. /// Raised to determine whether the door should automatically close.
/// Cancel to stop it from automatically closing. /// Cancel to stop it from automatically closing.
/// </summary> /// </summary>
/// <remarks>
/// This is called when a door decides whether it SHOULD auto close, not when it actually closes.
/// </remarks>
public class BeforeDoorAutoCloseEvent : CancellableEntityEventArgs public class BeforeDoorAutoCloseEvent : CancellableEntityEventArgs
{ {
} }
@@ -66,52 +65,17 @@ namespace Content.Shared.Doors
public float PryTimeModifier = 1.0f; public float PryTimeModifier = 1.0f;
} }
/// <summary>
/// Raised to determine how long the door's close time should be modified by.
/// Multiply CloseTimeModifier by the desired amount.
/// </summary>
public class DoorGetCloseTimeModifierEvent : EntityEventArgs
{
public float CloseTimeModifier = 1.0f;
}
/// <summary>
/// Raised to determine whether clicking the door should open/close it.
/// </summary>
public class DoorClickShouldActivateEvent : HandledEntityEventArgs
{
public ActivateEventArgs Args;
public DoorClickShouldActivateEvent(ActivateEventArgs args)
{
Args = args;
}
}
/// <summary> /// <summary>
/// Raised when an attempt to pry open the door is made. /// Raised when an attempt to pry open the door is made.
/// Cancel to stop the door from being pried open. /// Cancel to stop the door from being pried open.
/// </summary> /// </summary>
public class BeforeDoorPryEvent : CancellableEntityEventArgs public class BeforeDoorPryEvent : CancellableEntityEventArgs
{ {
public InteractUsingEventArgs Args; public readonly EntityUid User;
public BeforeDoorPryEvent(InteractUsingEventArgs args) public BeforeDoorPryEvent(EntityUid user)
{ {
Args = args; User = user;
}
}
/// <summary>
/// Raised when a door is successfully pried open.
/// </summary>
public class OnDoorPryEvent : EntityEventArgs
{
public InteractUsingEventArgs Args;
public OnDoorPryEvent(InteractUsingEventArgs args)
{
Args = args;
} }
} }
} }

View File

@@ -1,189 +0,0 @@
using System;
using System.Collections.Generic;
using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
using Robust.Shared.IoC;
using Robust.Shared.Physics;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.Timing;
using Robust.Shared.ViewVariables;
namespace Content.Shared.Doors
{
[NetworkedComponent]
public abstract class SharedDoorComponent : Component
{
public override string Name => "Door";
[Dependency]
protected readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly IEntityManager _entMan = default!;
[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>
[DataField("closeTimeOne")]
protected TimeSpan CloseTimeOne = TimeSpan.FromSeconds(0.4f);
/// <summary>
/// Closing time until fully closed.
/// </summary>
[DataField("closeTimeTwo")]
protected TimeSpan CloseTimeTwo = TimeSpan.FromSeconds(0.2f);
/// <summary>
/// Opening time until passable.
/// </summary>
[DataField("openTimeOne")]
protected TimeSpan OpenTimeOne = TimeSpan.FromSeconds(0.4f);
/// <summary>
/// Opening time until fully open.
/// </summary>
[DataField("openTimeTwo")]
protected TimeSpan OpenTimeTwo = TimeSpan.FromSeconds(0.2f);
/// <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 bool IsCrushing(EntityUid entity)
{
return CurrentlyCrushing.Contains(entity);
}
protected void SetAppearance(DoorVisualState state)
{
if (_entMan.TryGetComponent<AppearanceComponent>(Owner, out var appearanceComponent))
{
appearanceComponent.SetData(DoorVisuals.VisualState, state);
}
}
/// <summary>
/// Called when the door is partially opened.
/// </summary>
protected virtual void OnPartialOpen()
{
if (_entMan.TryGetComponent<PhysicsComponent>(Owner, out var physicsComponent))
{
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 (_entMan.TryGetComponent<PhysicsComponent>(Owner, out var physicsComponent))
{
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,
Powered,
BoltLights
}
[Serializable, NetSerializable]
public class DoorComponentState : ComponentState
{
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)
{
DoorState = doorState;
StartTime = startTime;
CurrentlyCrushing = currentlyCrushing;
CurTime = curTime;
}
}
public sealed class DoorOpenAttemptEvent : CancellableEntityEventArgs
{
}
public sealed class DoorCloseAttemptEvent : CancellableEntityEventArgs
{
}
}

View File

@@ -1,22 +0,0 @@
using Robust.Shared.GameObjects;
using Robust.Shared.Physics.Dynamics;
namespace Content.Shared.Doors
{
public abstract class SharedDoorSystem : EntitySystem
{
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<SharedDoorComponent, PreventCollideEvent>(PreventCollision);
}
private void PreventCollision(EntityUid uid, SharedDoorComponent component, PreventCollideEvent args)
{
if (component.IsCrushing(args.BodyB.Owner))
{
args.Cancel();
}
}
}
}

View File

@@ -0,0 +1,41 @@
using Content.Shared.Doors.Components;
using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
using Robust.Shared.IoC;
using System.Linq;
namespace Content.Shared.Doors.Systems;
public abstract class SharedAirlockSystem : EntitySystem
{
[Dependency] protected readonly SharedDoorSystem DoorSystem = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<SharedAirlockComponent, ComponentGetState>(OnGetState);
SubscribeLocalEvent<SharedAirlockComponent, ComponentHandleState>(OnHandleState);
SubscribeLocalEvent<SharedAirlockComponent, BeforeDoorClosedEvent>(OnBeforeDoorClosed);
}
private void OnGetState(EntityUid uid, SharedAirlockComponent airlock, ref ComponentGetState args)
{
// Need to network airlock safety state to avoid mis-predicts when a door auto-closes as the client walks through the door.
args.State = new AirlockComponentState(airlock.Safety);
}
private void OnHandleState(EntityUid uid, SharedAirlockComponent airlock, ref ComponentHandleState args)
{
if (args.Current is not AirlockComponentState state)
return;
airlock.Safety = state.Safety;
}
protected virtual void OnBeforeDoorClosed(EntityUid uid, SharedAirlockComponent airlock, BeforeDoorClosedEvent args)
{
if (airlock.Safety && DoorSystem.GetColliding(uid).Any())
args.Cancel();
}
}

View File

@@ -0,0 +1,579 @@
using Content.Shared.Access.Components;
using Content.Shared.Damage;
using Content.Shared.Doors.Components;
using Content.Shared.Examine;
using Content.Shared.Hands.Components;
using Content.Shared.Interaction;
using Content.Shared.Stunnable;
using Robust.Shared.Audio;
using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Log;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Dynamics;
using Robust.Shared.Timing;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Content.Shared.Doors.Systems;
public abstract class SharedDoorSystem : EntitySystem
{
[Dependency] private readonly SharedPhysicsSystem _physicsSystem = default!;
[Dependency] private readonly DamageableSystem _damageableSystem = default!;
[Dependency] private readonly SharedStunSystem _stunSystem = default!;
[Dependency] protected readonly IGameTiming GameTiming = default!;
/// <summary>
/// A body must have an intersection percentage larger than this in order to be considered as colliding with a
/// door. Used for safety close-blocking and crushing.
/// </summary>
/// <remarks>
/// The intersection percentage relies on WORLD AABBs. So if this is too small, and the grid is rotated 45
/// degrees, then an entity outside of the airlock may be crushed.
/// </remarks>
public const float IntersectPercentage = 0.2f;
/// <summary>
/// A set of doors that are currently opening, closing, or just queued to open/close after some delay.
/// </summary>
private readonly HashSet<DoorComponent> _activeDoors = new();
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<DoorComponent, ComponentStartup>(OnStartup);
SubscribeLocalEvent<DoorComponent, ComponentShutdown>(OnShutdown);
SubscribeLocalEvent<DoorComponent, ComponentGetState>(OnGetState);
SubscribeLocalEvent<DoorComponent, ComponentHandleState>(OnHandleState);
SubscribeLocalEvent<DoorComponent, ActivateInWorldEvent>(OnActivate);
SubscribeLocalEvent<DoorComponent, ExaminedEvent>(OnExamine);
SubscribeLocalEvent<DoorComponent, StartCollideEvent>(HandleCollide);
SubscribeLocalEvent<DoorComponent, PreventCollideEvent>(PreventCollision);
}
private void OnStartup(EntityUid uid, DoorComponent door, ComponentStartup args)
{
// if the door state is not standard (i.e., door starts open), make sure collision & occlusion are properly set.
if (door.StartOpen)
{
// disable occluder & physics
OnPartialOpen(uid, door);
// THEN set the correct state, inc disabling partial = true
SetState(uid, DoorState.Open, door);
// The airlock component may schedule an auto-close for this door during the SetState.
// Give the door is supposed to start open, let's prevent any auto-closing that might occur.
door.NextStateChange = null;
}
UpdateAppearance(uid, door);
}
private void OnShutdown(EntityUid uid, DoorComponent door, ComponentShutdown args)
{
_activeDoors.Remove(door);
}
#region StateManagement
private void OnGetState(EntityUid uid, DoorComponent door, ref ComponentGetState args)
{
args.State = new DoorComponentState(door);
}
private void OnHandleState(EntityUid uid, DoorComponent door, ref ComponentHandleState args)
{
if (args.Current is not DoorComponentState state)
return;
door.CurrentlyCrushing = state.CurrentlyCrushing;
door.State = state.DoorState;
door.NextStateChange = state.NextStateChange;
door.Partial = state.Partial;
if (state.NextStateChange == null)
_activeDoors.Remove(door);
else
_activeDoors.Add(door);
RaiseLocalEvent(uid, new DoorStateChangedEvent(door.State), false);
UpdateAppearance(uid, door);
}
protected void SetState(EntityUid uid, DoorState state, DoorComponent? door = null)
{
if (!Resolve(uid, ref door))
return;
switch (state)
{
case DoorState.Opening:
_activeDoors.Add(door);
door.NextStateChange = GameTiming.CurTime + door.OpenTimeOne;
break;
case DoorState.Closing:
_activeDoors.Add(door);
door.NextStateChange = GameTiming.CurTime + door.CloseTimeOne;
break;
case DoorState.Denying:
_activeDoors.Add(door);
door.NextStateChange = GameTiming.CurTime + door.DenyDuration;
break;
case DoorState.Open:
case DoorState.Closed:
door.Partial = false;
if (door.NextStateChange == null)
_activeDoors.Remove(door);
break;
}
door.State = state;
door.Dirty();
RaiseLocalEvent(uid, new DoorStateChangedEvent(state), false);
UpdateAppearance(uid, door);
}
protected virtual void UpdateAppearance(EntityUid uid, DoorComponent? door = null)
{
if (!Resolve(uid, ref door))
return;
if (!TryComp(uid, out AppearanceComponent? appearance))
return;
appearance.SetData(DoorVisuals.State, door.State);
}
#endregion
#region Interactions
private void OnActivate(EntityUid uid, DoorComponent door, ActivateInWorldEvent args)
{
if (args.Handled || !door.ClickOpen)
return;
TryToggleDoor(uid, door, args.User);
args.Handled = true;
}
private void OnExamine(EntityUid uid, DoorComponent door, ExaminedEvent args)
{
if (door.State == DoorState.Welded)
args.PushText(Loc.GetString("door-component-examine-is-welded"));
}
/// <summary>
/// Update the door state/visuals and play an access denied sound when a user without access interacts with the
/// door.
/// </summary>
public void Deny(EntityUid uid, DoorComponent? door = null, EntityUid? user = null, bool predicted = false)
{
if (!Resolve(uid, ref door))
return;
if (door.State != DoorState.Closed)
return;
// might not be able to deny without power or some other blocker.
var ev = new BeforeDoorDeniedEvent();
RaiseLocalEvent(uid, ev, false);
if (ev.Cancelled)
return;
SetState(uid, DoorState.Denying, door);
if (door.DenySound != null)
PlaySound(uid, door.DenySound.GetSound(), AudioParams.Default.WithVolume(-3), user, predicted);
}
public bool TryToggleDoor(EntityUid uid, DoorComponent? door = null, EntityUid? user = null, bool predicted = false)
{
if (!Resolve(uid, ref door))
return false;
if (door.State == DoorState.Closed)
{
return TryOpen(uid, door, user, predicted);
}
else if (door.State == DoorState.Open)
{
return TryClose(uid, door, user, predicted);
}
return false;
}
#endregion
#region Opening
public bool TryOpen(EntityUid uid, DoorComponent? door = null, EntityUid? user = null, bool predicted = false)
{
if (!Resolve(uid, ref door))
return false;
if (!CanOpen(uid, door, user, false))
return false;
StartOpening(uid, door, user, predicted);
return true;
}
public bool CanOpen(EntityUid uid, DoorComponent? door = null, EntityUid? user = null, bool quiet = true)
{
if (!Resolve(uid, ref door))
return false;
if (door.State == DoorState.Welded)
return false;
var ev = new BeforeDoorOpenedEvent();
RaiseLocalEvent(uid, ev, false);
if (ev.Cancelled)
return false;
if (!HasAccess(uid, user))
{
if (!quiet)
Deny(uid, door);
return false;
}
return true;
}
public virtual void StartOpening(EntityUid uid, DoorComponent? door = null, EntityUid? user = null, bool predicted = false)
{
if (!Resolve(uid, ref door))
return;
SetState(uid, DoorState.Opening, door);
if (door.OpenSound != null)
PlaySound(uid, door.OpenSound.GetSound(), AudioParams.Default.WithVolume(-5), user, predicted);
// 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 SharedHandsComponent? hands) && hands.Hands.Count == 0)
PlaySound(uid, door.TryOpenDoorSound.GetSound(), AudioParams.Default.WithVolume(-2), user, predicted);
}
/// <summary>
/// Called when the door is partially opened. The door becomes transparent and stops colliding with entities.
/// </summary>
public virtual void OnPartialOpen(EntityUid uid, DoorComponent? door = null, PhysicsComponent? physics = null)
{
if (!Resolve(uid, ref door, ref physics))
return;
// we can't be crushing anyone anymore, since we're opening
door.CurrentlyCrushing.Clear();
physics.CanCollide = false;
door.Partial = true;
door.NextStateChange = GameTiming.CurTime + door.CloseTimeTwo;
_activeDoors.Add(door);
door.Dirty();
if (door.Occludes && TryComp(uid, out OccluderComponent? occluder))
occluder.Enabled = false;
}
#endregion
#region Closing
public bool TryClose(EntityUid uid, DoorComponent? door = null, EntityUid? user = null, bool predicted = false)
{
if (!Resolve(uid, ref door))
return false;
if (!CanClose(uid, door, user, false))
return false;
StartClosing(uid, door, user, predicted);
return true;
}
public bool CanClose(EntityUid uid, DoorComponent? door = null, EntityUid? user = null, bool quiet = true)
{
if (!Resolve(uid, ref door))
return false;
var ev = new BeforeDoorClosedEvent();
RaiseLocalEvent(uid, ev, false);
if (ev.Cancelled)
return false;
return HasAccess(uid, user);
}
public virtual 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 (door.CloseSound != null)
PlaySound(uid, door.CloseSound.GetSound(), AudioParams.Default.WithVolume(-5), user, predicted);
}
/// <summary>
/// Called when the door is partially closed. This is when the door becomes "solid". If this process fails (e.g., a
/// mob entered the door as it was closing), then this returns false. Otherwise, returns true;
/// </summary>
public virtual bool OnPartialClose(EntityUid uid, DoorComponent? door = null, PhysicsComponent? physics = null)
{
if (!Resolve(uid, ref door, ref physics))
return false;
door.Partial = true;
door.Dirty();
// Make sure no entity waled into the airlock when it started closing.
if (!CanClose(uid, door))
{
door.NextStateChange = GameTiming.CurTime + door.OpenTimeTwo;
door.State = DoorState.Opening;
UpdateAppearance(uid, door);
return false;
}
physics.CanCollide = true;
door.NextStateChange = GameTiming.CurTime + door.CloseTimeTwo;
_activeDoors.Add(door);
if (door.Occludes && TryComp(uid, out OccluderComponent? occluder))
occluder.Enabled = true;
// Crush any entities. Note that we don't check airlock safety here. This should have been checked before
// the door closed.
Crush(uid, door, physics);
return true;
}
#endregion
#region Collisions
/// <summary>
/// Crushes everyone colliding with us by more than <see cref="IntersectPercentage"/>%.
/// </summary>
public void Crush(EntityUid uid, DoorComponent? door = null, PhysicsComponent? physics = null)
{
if (!Resolve(uid, ref door))
return;
// is this door capable of crushing? NOT the same as an airlock safety check. The door will still close.
if (!door.CanCrush)
return;
// Crush
var stunTime = door.DoorStunTime + door.OpenTimeOne;
foreach (var entity in GetColliding(uid, physics))
{
door.CurrentlyCrushing.Add(entity);
if (door.CrushDamage != null)
_damageableSystem.TryChangeDamage(entity, door.CrushDamage);
_stunSystem.TryParalyze(entity, stunTime, true);
}
if (door.CurrentlyCrushing.Count == 0)
return;
// queue the door to open so that the player is no longer stunned once it has FINISHED opening.
door.NextStateChange = GameTiming.CurTime + door.DoorStunTime;
door.Partial = false;
}
/// <summary>
/// Get all entities that collide with this door by more than <see cref="IntersectPercentage"/> percent.\
/// </summary>
public IEnumerable<EntityUid> GetColliding(EntityUid uid, PhysicsComponent? physics = null)
{
if (!Resolve(uid, ref physics))
yield break;
// TODO SLOTH fix electro's code.
var doorAABB = physics.GetWorldAABB();
foreach (var body in _physicsSystem.GetCollidingEntities(Transform(uid).MapID, doorAABB))
{
// static bodies (e.g., furniture) shouldn't stop airlocks/windoors from closing.
if (body.BodyType == BodyType.Static)
continue;
if (body.GetWorldAABB().IntersectPercentage(doorAABB) < IntersectPercentage)
continue;
yield return body.Owner;
}
}
private void PreventCollision(EntityUid uid, DoorComponent component, PreventCollideEvent args)
{
if (component.CurrentlyCrushing.Contains(args.BodyB.Owner))
{
args.Cancel();
}
}
protected virtual void HandleCollide(EntityUid uid, DoorComponent door, StartCollideEvent args)
{
// TODO ACCESS READER move access reader to shared and predict door opening/closing
// Then this can be moved to the shared system without mispredicting.
}
#endregion
#region Access
public virtual bool HasAccess(EntityUid uid, EntityUid? user = null, AccessReaderComponent? access = null)
{
// TODO network AccessComponent for predicting doors
// Currently all door open/close & door-bumper collision stuff is done server side.
// so this return value means nothing.
return true;
}
/// <summary>
/// Determines the base access behavior of all doors on the station.
/// </summary>
public AccessTypes AccessType = AccessTypes.Id;
/// <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
}
#endregion
#region Updating
/// <summary>
/// Schedule an open or closed door to progress to the next state after some time.
/// </summary>
/// <remarks>
/// If the requested delay is null or non-positive, this will make the door stay open or closed indefinitely.
/// </remarks>
public void SetNextStateChange(EntityUid uid, TimeSpan? delay, DoorComponent? door = null)
{
if (!Resolve(uid, ref door, false))
return;
// If the door is not currently just open or closed, it is busy doing something else (or welded shut). So in
// that case we do nothing.
if (door.State != DoorState.Open && door.State != DoorState.Closed)
return;
// Is this trying to prevent an update? (e.g., cancel an auto-close)
if (delay == null || delay.Value <= TimeSpan.Zero)
{
door.NextStateChange = null;
_activeDoors.Remove(door);
return;
}
door.NextStateChange = GameTiming.CurTime + delay.Value;
_activeDoors.Add(door);
}
/// <summary>
/// Iterate over active doors and progress them to the next state if they need to be updated.
/// </summary>
public override void Update(float frameTime)
{
var time = GameTiming.CurTime;
foreach (var door in _activeDoors.ToList())
{
if (door.Deleted || door.NextStateChange == null)
{
_activeDoors.Remove(door);
continue;
}
if (Paused(door.Owner))
continue;
if (door.NextStateChange.Value < time)
NextState(door, time);
}
}
/// <summary>
/// Makes a door proceed to the next state (if applicable).
/// </summary>
private void NextState(DoorComponent door, TimeSpan time)
{
door.NextStateChange = null;
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(door.Owner, door, predicted: true);
switch (door.State)
{
case DoorState.Opening:
// Either fully or partially open this door.
if (door.Partial)
SetState(door.Owner, DoorState.Open, door);
else
OnPartialOpen(door.Owner, door);
break;
case DoorState.Closing:
// Either fully or partially close this door.
if (door.Partial)
SetState(door.Owner, DoorState.Closed, door);
else
OnPartialClose(door.Owner, door);
break;
case DoorState.Denying:
// Finish denying entry and return to the closed state.
SetState(door.Owner, DoorState.Closed, door);
break;
case DoorState.Open:
// This door is open, and queued for an auto-close.
if (!TryClose(door.Owner, door, predicted: true))
{
// The door failed to close (blocked?). Try again in one second.
door.NextStateChange = time + TimeSpan.FromSeconds(1);
}
break;
case DoorState.Welded:
// A welded door? This should never have been active in the first place.
Logger.Error($"Welded door was in the list of active doors. Door: {ToPrettyString(door.Owner)}");
break;
}
}
#endregion
protected abstract void PlaySound(EntityUid uid, string sound, AudioParams audioParams, EntityUid? predictingPlayer, bool predicted);
}

View File

@@ -1,18 +1,18 @@
using System; using System;
using Content.Shared.Audio; using Content.Shared.Audio;
using Content.Shared.Hands;
using Content.Shared.Hands.Components; using Content.Shared.Hands.Components;
using Content.Shared.Rotation; using Content.Shared.Rotation;
using Robust.Shared.Audio; using Robust.Shared.Audio;
using Robust.Shared.GameObjects; using Robust.Shared.GameObjects;
using Robust.Shared.IoC; using Robust.Shared.IoC;
using Robust.Shared.Player; using Robust.Shared.Player;
using Robust.Shared.Timing;
namespace Content.Shared.Standing namespace Content.Shared.Standing
{ {
public sealed class StandingStateSystem : EntitySystem public sealed class StandingStateSystem : EntitySystem
{ {
[Dependency] private readonly SharedHandsSystem _sharedHandsSystem = default!; [Dependency] private readonly IGameTiming _gameTiming = default!;
public bool IsDown(EntityUid uid, StandingStateComponent? standingState = null) public bool IsDown(EntityUid uid, StandingStateComponent? standingState = null)
{ {
@@ -56,10 +56,14 @@ namespace Content.Shared.Standing
standingState.Dirty(); standingState.Dirty();
RaiseLocalEvent(uid, new DownedEvent(), false); RaiseLocalEvent(uid, new DownedEvent(), false);
if (!_gameTiming.IsFirstTimePredicted)
return true;
// Seemed like the best place to put it // Seemed like the best place to put it
appearance?.SetData(RotationVisuals.RotationState, RotationState.Horizontal); appearance?.SetData(RotationVisuals.RotationState, RotationState.Horizontal);
// Currently shit is only downed by server but when it's predicted we can probably only play this on server / client // Currently shit is only downed by server but when it's predicted we can probably only play this on server / client
// > no longer true with door crushing. There just needs to be a better way to handle audio prediction.
if (playSound) if (playSound)
{ {
SoundSystem.Play(Filter.Pvs(uid), standingState.DownSoundCollection.GetSound(), uid, AudioHelpers.WithVariation(0.25f)); SoundSystem.Play(Filter.Pvs(uid), standingState.DownSoundCollection.GetSound(), uid, AudioHelpers.WithVariation(0.25f));

View File

@@ -0,0 +1 @@
door-component-examine-is-welded = It has been welded shut.

View File

@@ -96,7 +96,6 @@
- type: Body - type: Body
template: HumanoidTemplate template: HumanoidTemplate
preset: HumanPreset preset: HumanPreset
- type: DoorBumpOpener
- type: entity - type: entity
save: false save: false

View File

@@ -10,6 +10,7 @@
tags: tags:
- CanPilot - CanPilot
- FootstepSound - FootstepSound
- DoorBumpOpener
- type: Reactive - type: Reactive
groups: groups:
Flammable: [ Touch ] Flammable: [ Touch ]
@@ -293,7 +294,6 @@
attributes: attributes:
proper: true proper: true
- type: StandingState - type: StandingState
- type: DoorBumpOpener
- type: entity - type: entity
save: false save: false
name: Urist McHands name: Urist McHands

View File

@@ -93,7 +93,6 @@
- type: Body - type: Body
template: HumanoidTemplate template: HumanoidTemplate
preset: SlimePreset preset: SlimePreset
- type: DoorBumpOpener
- type: HumanoidAppearance - type: HumanoidAppearance
hairMatchesSkin: true hairMatchesSkin: true
hairAlpha: 0.5 hairAlpha: 0.5

View File

@@ -113,6 +113,5 @@
categoriesFacialHair: VoxFacialHair categoriesFacialHair: VoxFacialHair
- type: Inventory - type: Inventory
speciesId: vox speciesId: vox
- type: DoorBumpOpener
- type: Butcherable - type: Butcherable
meat: FoodMeatChicken meat: FoodMeatChicken

View File

@@ -47,7 +47,9 @@
whitelist: whitelist:
components: components:
- IdCard - IdCard
- type: DoorBumpOpener - type: Tag
tags:
- DoorBumpOpener
- type: entity - type: entity
parent: BasePDA parent: BasePDA

View File

@@ -14,7 +14,9 @@
HeldPrefix: default HeldPrefix: default
- type: Access - type: Access
- type: IdCard - type: IdCard
- type: DoorBumpOpener - type: Tag
tags:
- DoorBumpOpener
#IDs with layers #IDs with layers

View File

@@ -42,8 +42,6 @@
map: ["enum.DoorVisualLayers.BaseBolted"] map: ["enum.DoorVisualLayers.BaseBolted"]
- state: panel_open - state: panel_open
map: ["enum.WiresVisualLayers.MaintenancePanel"] map: ["enum.WiresVisualLayers.MaintenancePanel"]
- type: Physics
canCollide: false
- type: Fixtures - type: Fixtures
fixtures: fixtures:
- shape: - shape:
@@ -65,7 +63,7 @@
openTimeTwo: 0.6 openTimeTwo: 0.6
startOpen: true startOpen: true
bumpOpen: false bumpOpen: false
inhibitCrush: false clickOpen: false
crushDamage: crushDamage:
types: types:
Blunt: 15 Blunt: 15
@@ -90,8 +88,6 @@
type: WiresBoundUserInterface type: WiresBoundUserInterface
- type: Airtight - type: Airtight
fixVacuum: true fixVacuum: true
- type: Occluder
enabled: false
- type: Construction - type: Construction
graph: Firelock graph: Firelock
node: Firelock node: Firelock
@@ -101,14 +97,6 @@
parent: Firelock parent: Firelock
name: glass firelock name: glass firelock
components: components:
- type: Door
occludes: false
inhibitCrush: false
crushDamage:
types:
Blunt: 15
- type: Occluder
enabled: false
- type: Sprite - type: Sprite
sprite: Structures/Doors/Airlocks/Glass/firelock.rsi sprite: Structures/Doors/Airlocks/Glass/firelock.rsi
@@ -117,12 +105,6 @@
parent: Firelock parent: Firelock
name: firelock name: firelock
components: components:
- type: Door
occludes: false
inhibitCrush: false
crushDamage:
types:
Blunt: 15
- type: Sprite - type: Sprite
sprite: Structures/Doors/edge_door_hazard.rsi sprite: Structures/Doors/edge_door_hazard.rsi
- type: Airtight - type: Airtight
@@ -130,7 +112,6 @@
noAirWhenFullyAirBlocked: false noAirWhenFullyAirBlocked: false
airBlockedDirection: airBlockedDirection:
- South - South
- type: Physics
- type: Fixtures - type: Fixtures
fixtures: fixtures:
- shape: - shape:

View File

@@ -30,12 +30,10 @@
board: DoorElectronics board: DoorElectronics
bumpOpen: false bumpOpen: false
clickOpen: false clickOpen: false
autoClose: false
closeTimeOne: 0.2 closeTimeOne: 0.2
closeTimeTwo: 1.2 closeTimeTwo: 1.2
openTimeOne: 1.2 openTimeOne: 1.2
openTimeTwo: 0.2 openTimeTwo: 0.2
inhibitCrush: false
crushDamage: crushDamage:
types: types:
Blunt: 5 # getting shutters closed on you probably doesn't hurt that much Blunt: 5 # getting shutters closed on you probably doesn't hurt that much

View File

@@ -102,6 +102,9 @@
- type: Tag - type: Tag
id: Document id: Document
- type: Tag
id: DoorBumpOpener
- type: Tag - type: Tag
id: DoorElectronics id: DoorElectronics