SharedDoorSystem.cs 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821
  1. using System.Linq;
  2. using Content.Shared.Access.Components;
  3. using Content.Shared.Access.Systems;
  4. using Content.Shared.Administration.Logs;
  5. using Content.Shared.Damage;
  6. using Content.Shared.Database;
  7. using Content.Shared.Doors.Components;
  8. using Content.Shared.Emag.Systems;
  9. using Content.Shared.Interaction;
  10. using Content.Shared.Physics;
  11. using Content.Shared.Popups;
  12. using Content.Shared.Power.EntitySystems;
  13. using Content.Shared.Prying.Components;
  14. using Content.Shared.Prying.Systems;
  15. using Content.Shared.Stunnable;
  16. using Content.Shared.Tag;
  17. using Content.Shared.Tools.Systems;
  18. using Robust.Shared.Audio;
  19. using Robust.Shared.Physics.Components;
  20. using Robust.Shared.Physics.Events;
  21. using Robust.Shared.Physics.Systems;
  22. using Robust.Shared.Timing;
  23. using Robust.Shared.Audio.Systems;
  24. using Robust.Shared.Network;
  25. using Robust.Shared.Map.Components;
  26. namespace Content.Shared.Doors.Systems;
  27. public abstract partial class SharedDoorSystem : EntitySystem
  28. {
  29. [Dependency] private readonly ISharedAdminLogManager _adminLog = default!;
  30. [Dependency] protected readonly IGameTiming GameTiming = default!;
  31. [Dependency] private readonly INetManager _net = default!;
  32. [Dependency] protected readonly SharedPhysicsSystem PhysicsSystem = default!;
  33. [Dependency] private readonly DamageableSystem _damageableSystem = default!;
  34. [Dependency] private readonly EmagSystem _emag = default!;
  35. [Dependency] private readonly SharedStunSystem _stunSystem = default!;
  36. [Dependency] protected readonly TagSystem Tags = default!;
  37. [Dependency] protected readonly SharedAudioSystem Audio = default!;
  38. [Dependency] private readonly EntityLookupSystem _entityLookup = default!;
  39. [Dependency] protected readonly SharedAppearanceSystem AppearanceSystem = default!;
  40. [Dependency] private readonly OccluderSystem _occluder = default!;
  41. [Dependency] private readonly AccessReaderSystem _accessReaderSystem = default!;
  42. [Dependency] private readonly PryingSystem _pryingSystem = default!;
  43. [Dependency] protected readonly SharedPopupSystem Popup = default!;
  44. [Dependency] private readonly SharedMapSystem _mapSystem = default!;
  45. [Dependency] private readonly SharedPowerReceiverSystem _powerReceiver = default!;
  46. [ValidatePrototypeId<TagPrototype>]
  47. public const string DoorBumpTag = "DoorBumpOpener";
  48. /// <summary>
  49. /// A set of doors that are currently opening, closing, or just queued to open/close after some delay.
  50. /// </summary>
  51. private readonly HashSet<Entity<DoorComponent>> _activeDoors = new();
  52. private readonly HashSet<Entity<PhysicsComponent>> _doorIntersecting = new();
  53. public override void Initialize()
  54. {
  55. base.Initialize();
  56. InitializeBolts();
  57. SubscribeLocalEvent<DoorComponent, ComponentInit>(OnComponentInit);
  58. SubscribeLocalEvent<DoorComponent, ComponentRemove>(OnRemove);
  59. SubscribeLocalEvent<DoorComponent, AfterAutoHandleStateEvent>(OnHandleState);
  60. SubscribeLocalEvent<DoorComponent, ActivateInWorldEvent>(OnActivate);
  61. SubscribeLocalEvent<DoorComponent, StartCollideEvent>(HandleCollide);
  62. SubscribeLocalEvent<DoorComponent, PreventCollideEvent>(PreventCollision);
  63. SubscribeLocalEvent<DoorComponent, BeforePryEvent>(OnBeforePry);
  64. SubscribeLocalEvent<DoorComponent, PriedEvent>(OnAfterPry);
  65. SubscribeLocalEvent<DoorComponent, WeldableAttemptEvent>(OnWeldAttempt);
  66. SubscribeLocalEvent<DoorComponent, WeldableChangedEvent>(OnWeldChanged);
  67. SubscribeLocalEvent<DoorComponent, GetPryTimeModifierEvent>(OnPryTimeModifier);
  68. SubscribeLocalEvent<DoorComponent, GotEmaggedEvent>(OnEmagged);
  69. }
  70. protected virtual void OnComponentInit(Entity<DoorComponent> ent, ref ComponentInit args)
  71. {
  72. var door = ent.Comp;
  73. if (door.NextStateChange != null)
  74. _activeDoors.Add(ent);
  75. else
  76. {
  77. // Make sure doors are not perpetually stuck opening or closing.
  78. if (door.State == DoorState.Opening)
  79. {
  80. // force to open.
  81. door.State = DoorState.Open;
  82. door.Partial = false;
  83. }
  84. if (door.State == DoorState.Closing)
  85. {
  86. // force to closed.
  87. door.State = DoorState.Closed;
  88. door.Partial = false;
  89. }
  90. }
  91. // should this door have collision and the like enabled?
  92. var collidable = door.State == DoorState.Closed
  93. || door.State == DoorState.Closing && door.Partial
  94. || door.State == DoorState.Opening && !door.Partial;
  95. SetCollidable(ent, collidable, door);
  96. AppearanceSystem.SetData(ent, DoorVisuals.State, door.State);
  97. }
  98. private void OnRemove(Entity<DoorComponent> door, ref ComponentRemove args)
  99. {
  100. _activeDoors.Remove(door);
  101. }
  102. private void OnEmagged(EntityUid uid, DoorComponent door, ref GotEmaggedEvent args)
  103. {
  104. if (!_emag.CompareFlag(args.Type, EmagType.Access))
  105. return;
  106. if (!TryComp<AirlockComponent>(uid, out var airlock))
  107. return;
  108. if (IsBolted(uid) || !airlock.Powered)
  109. return;
  110. if (door.State != DoorState.Closed)
  111. return;
  112. if (!SetState(uid, DoorState.Emagging, door))
  113. return;
  114. args.Repeatable = true;
  115. args.Handled = true;
  116. }
  117. #region StateManagement
  118. private void OnHandleState(Entity<DoorComponent> ent, ref AfterAutoHandleStateEvent args)
  119. {
  120. var door = ent.Comp;
  121. if (door.NextStateChange == null)
  122. _activeDoors.Remove(ent);
  123. else
  124. _activeDoors.Add(ent);
  125. RaiseLocalEvent(ent, new DoorStateChangedEvent(door.State));
  126. }
  127. public bool SetState(EntityUid uid, DoorState state, DoorComponent? door = null)
  128. {
  129. if (!Resolve(uid, ref door))
  130. return false;
  131. // If no change, return to avoid firing a new DoorStateChangedEvent.
  132. if (state == door.State)
  133. return false;
  134. switch (state)
  135. {
  136. case DoorState.Opening:
  137. _activeDoors.Add((uid, door));
  138. door.NextStateChange = GameTiming.CurTime + door.OpenTimeOne;
  139. break;
  140. case DoorState.Closing:
  141. _activeDoors.Add((uid, door));
  142. door.NextStateChange = GameTiming.CurTime + door.CloseTimeOne;
  143. break;
  144. case DoorState.Denying:
  145. _activeDoors.Add((uid, door));
  146. door.NextStateChange = GameTiming.CurTime + door.DenyDuration;
  147. break;
  148. case DoorState.Emagging:
  149. _activeDoors.Add((uid, door));
  150. door.NextStateChange = GameTiming.CurTime + door.EmagDuration;
  151. break;
  152. case DoorState.Open:
  153. door.Partial = false;
  154. if (door.NextStateChange == null)
  155. _activeDoors.Remove((uid, door));
  156. break;
  157. case DoorState.Closed:
  158. // May want to keep the door around to re-check for opening if we got a contact during closing.
  159. door.Partial = false;
  160. break;
  161. }
  162. door.State = state;
  163. Dirty(uid, door);
  164. RaiseLocalEvent(uid, new DoorStateChangedEvent(state));
  165. AppearanceSystem.SetData(uid, DoorVisuals.State, door.State);
  166. return true;
  167. }
  168. #endregion
  169. #region Interactions
  170. protected void OnActivate(EntityUid uid, DoorComponent door, ActivateInWorldEvent args)
  171. {
  172. if (args.Handled || !args.Complex || !door.ClickOpen)
  173. return;
  174. if (!TryToggleDoor(uid, door, args.User, predicted: true))
  175. _pryingSystem.TryPry(uid, args.User, out _);
  176. args.Handled = true;
  177. }
  178. private void OnPryTimeModifier(EntityUid uid, DoorComponent door, ref GetPryTimeModifierEvent args)
  179. {
  180. args.BaseTime = door.PryTime;
  181. }
  182. private void OnBeforePry(EntityUid uid, DoorComponent door, ref BeforePryEvent args)
  183. {
  184. if (door.State == DoorState.Welded || !door.CanPry)
  185. args.Cancelled = true;
  186. }
  187. /// <summary>
  188. /// Open or close a door after it has been successfully pried.
  189. /// </summary>
  190. private void OnAfterPry(EntityUid uid, DoorComponent door, ref PriedEvent args)
  191. {
  192. if (door.State == DoorState.Closed)
  193. {
  194. _adminLog.Add(LogType.Action, LogImpact.Medium, $"{ToPrettyString(args.User)} pried {ToPrettyString(uid)} open");
  195. StartOpening(uid, door, args.User, true);
  196. }
  197. else if (door.State == DoorState.Open)
  198. {
  199. _adminLog.Add(LogType.Action, LogImpact.Medium, $"{ToPrettyString(args.User)} pried {ToPrettyString(uid)} closed");
  200. StartClosing(uid, door, args.User, true);
  201. }
  202. }
  203. private void OnWeldAttempt(EntityUid uid, DoorComponent component, WeldableAttemptEvent args)
  204. {
  205. if (component.CurrentlyCrushing.Count > 0)
  206. {
  207. args.Cancel();
  208. return;
  209. }
  210. if (component.State != DoorState.Closed && component.State != DoorState.Welded)
  211. {
  212. args.Cancel();
  213. }
  214. }
  215. private void OnWeldChanged(EntityUid uid, DoorComponent component, ref WeldableChangedEvent args)
  216. {
  217. if (component.State == DoorState.Closed)
  218. SetState(uid, DoorState.Welded, component);
  219. else if (component.State == DoorState.Welded)
  220. SetState(uid, DoorState.Closed, component);
  221. }
  222. /// <summary>
  223. /// Update the door state/visuals and play an access denied sound when a user without access interacts with the
  224. /// door.
  225. /// </summary>
  226. public void Deny(EntityUid uid, DoorComponent? door = null, EntityUid? user = null, bool predicted = false)
  227. {
  228. if (!Resolve(uid, ref door))
  229. return;
  230. if (door.State != DoorState.Closed)
  231. return;
  232. // might not be able to deny without power or some other blocker.
  233. var ev = new BeforeDoorDeniedEvent();
  234. RaiseLocalEvent(uid, ev);
  235. if (ev.Cancelled)
  236. return;
  237. if (!SetState(uid, DoorState.Denying, door))
  238. return;
  239. if (predicted)
  240. Audio.PlayPredicted(door.DenySound, uid, user, AudioParams.Default.WithVolume(-3));
  241. else if (_net.IsServer)
  242. Audio.PlayPvs(door.DenySound, uid, AudioParams.Default.WithVolume(-3));
  243. }
  244. public bool TryToggleDoor(EntityUid uid, DoorComponent? door = null, EntityUid? user = null, bool predicted = false)
  245. {
  246. if (!Resolve(uid, ref door))
  247. return false;
  248. if (door.State is DoorState.Closed or DoorState.Denying)
  249. {
  250. return TryOpen(uid, door, user, predicted, quiet: door.State == DoorState.Denying);
  251. }
  252. if (door.State == DoorState.Open)
  253. {
  254. return TryClose(uid, door, user, predicted);
  255. }
  256. return false;
  257. }
  258. #endregion
  259. #region Opening
  260. public bool TryOpen(EntityUid uid, DoorComponent? door = null, EntityUid? user = null, bool predicted = false, bool quiet = false)
  261. {
  262. if (!Resolve(uid, ref door))
  263. return false;
  264. if (!CanOpen(uid, door, user, quiet))
  265. return false;
  266. StartOpening(uid, door, user, predicted);
  267. return true;
  268. }
  269. public bool CanOpen(EntityUid uid, DoorComponent? door = null, EntityUid? user = null, bool quiet = true)
  270. {
  271. if (!Resolve(uid, ref door))
  272. return false;
  273. if (door.State == DoorState.Welded)
  274. return false;
  275. var ev = new BeforeDoorOpenedEvent() { User = user };
  276. RaiseLocalEvent(uid, ev);
  277. if (ev.Cancelled)
  278. return false;
  279. if (!HasAccess(uid, user, door))
  280. {
  281. if (!quiet)
  282. Deny(uid, door, user, predicted: true);
  283. return false;
  284. }
  285. return true;
  286. }
  287. /// <summary>
  288. /// Immediately start opening a door
  289. /// </summary>
  290. /// <param name="uid"> The uid of the door</param>
  291. /// <param name="door"> The doorcomponent of the door</param>
  292. /// <param name="user"> The user (if any) opening the door</param>
  293. /// <param name="predicted">Whether the interaction would have been
  294. /// predicted. See comments in the PlaySound method on the Server system for details</param>
  295. public void StartOpening(EntityUid uid, DoorComponent? door = null, EntityUid? user = null, bool predicted = false)
  296. {
  297. if (!Resolve(uid, ref door))
  298. return;
  299. var lastState = door.State;
  300. if (!SetState(uid, DoorState.Opening, door))
  301. return;
  302. if (predicted)
  303. Audio.PlayPredicted(door.OpenSound, uid, user, AudioParams.Default.WithVolume(-5));
  304. else if (_net.IsServer)
  305. Audio.PlayPvs(door.OpenSound, uid, AudioParams.Default.WithVolume(-5));
  306. if (lastState == DoorState.Emagging && TryComp<DoorBoltComponent>(uid, out var doorBoltComponent))
  307. SetBoltsDown((uid, doorBoltComponent), !doorBoltComponent.BoltsDown, user, true);
  308. }
  309. /// <summary>
  310. /// Called when the door is partially opened. The door becomes transparent and stops colliding with entities.
  311. /// </summary>
  312. public void OnPartialOpen(EntityUid uid, DoorComponent? door = null)
  313. {
  314. if (!Resolve(uid, ref door))
  315. return;
  316. SetCollidable(uid, false, door);
  317. door.Partial = true;
  318. door.NextStateChange = GameTiming.CurTime + door.CloseTimeTwo;
  319. _activeDoors.Add((uid, door));
  320. Dirty(uid, door);
  321. }
  322. /// <summary>
  323. /// Opens and then bolts a door.
  324. /// Different from emagging this does not remove the access reader, so it can be repaired by simply unbolting the door.
  325. /// </summary>
  326. public bool TryOpenAndBolt(EntityUid uid, DoorComponent? door = null, AirlockComponent? airlock = null)
  327. {
  328. if (!Resolve(uid, ref door, ref airlock))
  329. return false;
  330. if (IsBolted(uid) || !airlock.Powered || door.State != DoorState.Closed)
  331. {
  332. return false;
  333. }
  334. SetState(uid, DoorState.Emagging, door);
  335. return true;
  336. }
  337. #endregion
  338. #region Closing
  339. public bool TryClose(EntityUid uid, DoorComponent? door = null, EntityUid? user = null, bool predicted = false)
  340. {
  341. if (!Resolve(uid, ref door))
  342. return false;
  343. if (!CanClose(uid, door, user))
  344. return false;
  345. StartClosing(uid, door, user, predicted);
  346. return true;
  347. }
  348. /// <summary>
  349. /// Immediately start closing a door
  350. /// </summary>
  351. /// <param name="uid"> The uid of the door</param>
  352. /// <param name="door"> The doorcomponent of the door</param>
  353. /// <param name="user"> The user (if any) opening the door</param>
  354. public bool CanClose(EntityUid uid, DoorComponent? door = null, EntityUid? user = null, bool partial = false)
  355. {
  356. if (!Resolve(uid, ref door))
  357. return false;
  358. // since both closing/closed and welded are door states, we need to prevent 'closing'
  359. // a welded door or else there will be weird state bugs
  360. if (door.State is DoorState.Welded or DoorState.Closed)
  361. return false;
  362. var ev = new BeforeDoorClosedEvent(door.PerformCollisionCheck, partial);
  363. RaiseLocalEvent(uid, ev);
  364. if (ev.Cancelled)
  365. return false;
  366. if (!HasAccess(uid, user, door))
  367. return false;
  368. return !ev.PerformCollisionCheck || !GetColliding(uid).Any();
  369. }
  370. public void StartClosing(EntityUid uid, DoorComponent? door = null, EntityUid? user = null, bool predicted = false)
  371. {
  372. if (!Resolve(uid, ref door))
  373. return;
  374. if (!SetState(uid, DoorState.Closing, door))
  375. return;
  376. if (predicted)
  377. Audio.PlayPredicted(door.CloseSound, uid, user, AudioParams.Default.WithVolume(-5));
  378. else if (_net.IsServer)
  379. Audio.PlayPvs(door.CloseSound, uid, AudioParams.Default.WithVolume(-5));
  380. }
  381. /// <summary>
  382. /// Called when the door is partially closed. This is when the door becomes "solid". If this process fails (e.g., a
  383. /// mob entered the door as it was closing), then this returns false. Otherwise, returns true;
  384. /// </summary>
  385. public bool OnPartialClose(EntityUid uid, DoorComponent? door = null, PhysicsComponent? physics = null)
  386. {
  387. if (!Resolve(uid, ref door, ref physics))
  388. return false;
  389. // Make sure no entity walked into the airlock when it started closing.
  390. if (!CanClose(uid, door, partial: true))
  391. {
  392. door.NextStateChange = GameTiming.CurTime + door.OpenTimeTwo;
  393. door.State = DoorState.Open;
  394. AppearanceSystem.SetData(uid, DoorVisuals.State, DoorState.Open);
  395. Dirty(uid, door);
  396. return false;
  397. }
  398. door.Partial = true;
  399. SetCollidable(uid, true, door, physics);
  400. door.NextStateChange = GameTiming.CurTime + door.CloseTimeTwo;
  401. Dirty(uid, door);
  402. _activeDoors.Add((uid, door));
  403. // Crush any entities. Note that we don't check airlock safety here. This should have been checked before
  404. // the door closed.
  405. Crush(uid, door, physics);
  406. return true;
  407. }
  408. #endregion
  409. #region Collisions
  410. protected virtual void SetCollidable(
  411. EntityUid uid,
  412. bool collidable,
  413. DoorComponent? door = null,
  414. PhysicsComponent? physics = null,
  415. OccluderComponent? occluder = null)
  416. {
  417. if (!Resolve(uid, ref door))
  418. return;
  419. if (Resolve(uid, ref physics, false))
  420. PhysicsSystem.SetCanCollide(uid, collidable, body: physics);
  421. if (!collidable)
  422. door.CurrentlyCrushing.Clear();
  423. if (door.Occludes)
  424. _occluder.SetEnabled(uid, collidable, occluder);
  425. }
  426. /// <summary>
  427. /// Crushes everyone colliding with us by more than <see cref="IntersectPercentage"/>%.
  428. /// </summary>
  429. public void Crush(EntityUid uid, DoorComponent? door = null, PhysicsComponent? physics = null)
  430. {
  431. if (!Resolve(uid, ref door))
  432. return;
  433. if (!door.CanCrush)
  434. return;
  435. // Find entities and apply curshing effects
  436. var stunTime = door.DoorStunTime + door.OpenTimeOne;
  437. foreach (var entity in GetColliding(uid, physics))
  438. {
  439. door.CurrentlyCrushing.Add(entity);
  440. if (door.CrushDamage != null)
  441. _damageableSystem.TryChangeDamage(entity, door.CrushDamage, origin: uid);
  442. _stunSystem.TryParalyze(entity, stunTime, true);
  443. }
  444. if (door.CurrentlyCrushing.Count == 0)
  445. return;
  446. // queue the door to open so that the player is no longer stunned once it has FINISHED opening.
  447. door.NextStateChange = GameTiming.CurTime + door.DoorStunTime;
  448. door.Partial = false;
  449. }
  450. /// <summary>
  451. /// Get all entities that collide with this door by more than <see cref="IntersectPercentage"/> percent.\
  452. /// </summary>
  453. public IEnumerable<EntityUid> GetColliding(EntityUid uid, PhysicsComponent? physics = null)
  454. {
  455. if (!Resolve(uid, ref physics))
  456. yield break;
  457. var xform = Transform(uid);
  458. // Getting the world bounds from the gridUid allows us to use the version of
  459. // GetCollidingEntities that returns Entity<PhysicsComponent>
  460. if (!TryComp<MapGridComponent>(xform.GridUid, out var mapGridComp))
  461. yield break;
  462. var tileRef = _mapSystem.GetTileRef(xform.GridUid.Value, mapGridComp, xform.Coordinates);
  463. _doorIntersecting.Clear();
  464. _entityLookup.GetLocalEntitiesIntersecting(xform.GridUid.Value, tileRef.GridIndices, _doorIntersecting, gridComp: mapGridComp, flags: (LookupFlags.All & ~LookupFlags.Sensors));
  465. // TODO SLOTH fix electro's code.
  466. // ReSharper disable once InconsistentNaming
  467. foreach (var otherPhysics in _doorIntersecting)
  468. {
  469. if (otherPhysics.Comp == physics)
  470. continue;
  471. if (!otherPhysics.Comp.CanCollide)
  472. continue;
  473. //TODO: Make only shutters ignore these objects upon colliding instead of all airlocks
  474. // Excludes Glasslayer for windows, GlassAirlockLayer for windoors, TableLayer for tables
  475. if (otherPhysics.Comp.CollisionLayer == (int) CollisionGroup.GlassLayer || otherPhysics.Comp.CollisionLayer == (int) CollisionGroup.GlassAirlockLayer || otherPhysics.Comp.CollisionLayer == (int) CollisionGroup.TableLayer)
  476. continue;
  477. // Ignore low-passable entities.
  478. if ((otherPhysics.Comp.CollisionMask & (int)CollisionGroup.LowImpassable) == 0)
  479. continue;
  480. //For when doors need to close over conveyor belts
  481. if (otherPhysics.Comp.CollisionLayer == (int) CollisionGroup.ConveyorMask)
  482. continue;
  483. if ((physics.CollisionMask & otherPhysics.Comp.CollisionLayer) == 0 && (otherPhysics.Comp.CollisionMask & physics.CollisionLayer) == 0)
  484. continue;
  485. yield return otherPhysics.Owner;
  486. }
  487. }
  488. private void PreventCollision(EntityUid uid, DoorComponent component, ref PreventCollideEvent args)
  489. {
  490. if (component.CurrentlyCrushing.Contains(args.OtherEntity))
  491. {
  492. args.Cancelled = true;
  493. }
  494. }
  495. /// <summary>
  496. /// Open a door if a player or door-bumper (PDA, ID-card) collide with the door. Sadly, bullets no longer
  497. /// generate "access denied" sounds as you fire at a door.
  498. /// </summary>
  499. private void HandleCollide(EntityUid uid, DoorComponent door, ref StartCollideEvent args)
  500. {
  501. if (!door.BumpOpen)
  502. return;
  503. if (door.State is not (DoorState.Closed or DoorState.Denying))
  504. return;
  505. var otherUid = args.OtherEntity;
  506. if (Tags.HasTag(otherUid, DoorBumpTag))
  507. TryOpen(uid, door, otherUid, quiet: door.State == DoorState.Denying, predicted: true);
  508. }
  509. #endregion
  510. #region Access
  511. /// <summary>
  512. /// Does the user have the permissions required to open this door?
  513. /// </summary>
  514. public bool HasAccess(EntityUid uid, EntityUid? user = null, DoorComponent? door = null, AccessReaderComponent? access = null)
  515. {
  516. // TODO network AccessComponent for predicting doors
  517. // if there is no "user" we skip the access checks. Access is also ignored in some game-modes.
  518. if (user == null || AccessType == AccessTypes.AllowAll)
  519. return true;
  520. // If the door is on emergency access we skip the checks.
  521. if (TryComp<AirlockComponent>(uid, out var airlock) && airlock.EmergencyAccess)
  522. return true;
  523. // Anyone can click to open firelocks
  524. if (Resolve(uid, ref door) && door.State == DoorState.Closed &&
  525. TryComp<FirelockComponent>(uid, out var firelock))
  526. return true;
  527. if (!Resolve(uid, ref access, false))
  528. return true;
  529. var isExternal = access.AccessLists.Any(list => list.Contains("External"));
  530. return AccessType switch
  531. {
  532. // Some game modes modify access rules.
  533. AccessTypes.AllowAllIdExternal => !isExternal || _accessReaderSystem.IsAllowed(user.Value, uid, access),
  534. AccessTypes.AllowAllNoExternal => !isExternal,
  535. _ => _accessReaderSystem.IsAllowed(user.Value, uid, access)
  536. };
  537. }
  538. /// <summary>
  539. /// Determines the base access behavior of all doors on the station.
  540. /// </summary>
  541. public AccessTypes AccessType = AccessTypes.Id;
  542. /// <summary>
  543. /// How door access should be handled.
  544. /// </summary>
  545. public enum AccessTypes
  546. {
  547. /// <summary> ID based door access. </summary>
  548. Id,
  549. /// <summary>
  550. /// Allows everyone to open doors, except external which airlocks are still handled with ID's
  551. /// </summary>
  552. AllowAllIdExternal,
  553. /// <summary>
  554. /// Allows everyone to open doors, except external airlocks which are never allowed, even if the user has
  555. /// ID access.
  556. /// </summary>
  557. AllowAllNoExternal,
  558. /// <summary> Allows everyone to open all doors. </summary>
  559. AllowAll
  560. }
  561. #endregion
  562. #region Updating
  563. /// <summary>
  564. /// Schedule an open or closed door to progress to the next state after some time.
  565. /// </summary>
  566. /// <remarks>
  567. /// If the requested delay is null or non-positive, this will make the door stay open or closed indefinitely.
  568. /// </remarks>
  569. public void SetNextStateChange(EntityUid uid, TimeSpan? delay, DoorComponent? door = null)
  570. {
  571. if (!Resolve(uid, ref door, false))
  572. return;
  573. // If the door is not currently just open or closed, it is busy doing something else (or welded shut). So in
  574. // that case we do nothing.
  575. if (door.State != DoorState.Open && door.State != DoorState.Closed)
  576. return;
  577. // Is this trying to prevent an update? (e.g., cancel an auto-close)
  578. if (delay == null || delay.Value <= TimeSpan.Zero)
  579. {
  580. door.NextStateChange = null;
  581. _activeDoors.Remove((uid, door));
  582. return;
  583. }
  584. door.NextStateChange = GameTiming.CurTime + delay.Value;
  585. Dirty(uid, door);
  586. _activeDoors.Add((uid, door));
  587. }
  588. protected void CheckDoorBump(Entity<DoorComponent, PhysicsComponent> ent)
  589. {
  590. var (uid, door, physics) = ent;
  591. if (door.BumpOpen)
  592. {
  593. foreach (var other in PhysicsSystem.GetContactingEntities(uid, physics))
  594. {
  595. if (Tags.HasTag(other, DoorBumpTag) && TryOpen(uid, door, other, quiet: true))
  596. break;
  597. }
  598. }
  599. }
  600. /// <summary>
  601. /// Iterate over active doors and progress them to the next state if they need to be updated.
  602. /// </summary>
  603. public override void Update(float frameTime)
  604. {
  605. var time = GameTiming.CurTime;
  606. foreach (var ent in _activeDoors.ToList())
  607. {
  608. var door = ent.Comp;
  609. if (door.Deleted || door.NextStateChange == null)
  610. {
  611. _activeDoors.Remove(ent);
  612. continue;
  613. }
  614. if (Paused(ent))
  615. continue;
  616. if (door.NextStateChange.Value < time)
  617. NextState(ent, time);
  618. if (door.State == DoorState.Closed &&
  619. TryComp<PhysicsComponent>(ent, out var doorBody))
  620. {
  621. // If something bumped into us during closing then start to re-open, otherwise, remove it from active.
  622. _activeDoors.Remove(ent);
  623. CheckDoorBump((ent, door, doorBody));
  624. }
  625. }
  626. }
  627. /// <summary>
  628. /// Makes a door proceed to the next state (if applicable).
  629. /// </summary>
  630. private void NextState(Entity<DoorComponent> ent, TimeSpan time)
  631. {
  632. var door = ent.Comp;
  633. door.NextStateChange = null;
  634. if (door.CurrentlyCrushing.Count > 0)
  635. // This is a closed door that is crushing people and needs to auto-open. Note that we don't check "can open"
  636. // here. The door never actually finished closing and we don't want people to get stuck inside of doors.
  637. StartOpening(ent, door);
  638. switch (door.State)
  639. {
  640. case DoorState.Opening:
  641. // Either fully or partially open this door.
  642. if (door.Partial)
  643. SetState(ent, DoorState.Open, door);
  644. else
  645. OnPartialOpen(ent, door);
  646. break;
  647. case DoorState.Closing:
  648. // Either fully or partially close this door.
  649. if (door.Partial)
  650. SetState(ent, DoorState.Closed, door);
  651. else
  652. OnPartialClose(ent, door);
  653. break;
  654. case DoorState.Denying:
  655. // Finish denying entry and return to the closed state.
  656. SetState(ent, DoorState.Closed, door);
  657. break;
  658. case DoorState.Emagging:
  659. StartOpening(ent, door);
  660. break;
  661. case DoorState.Open:
  662. // This door is open, and queued for an auto-close.
  663. if (!TryClose(ent, door))
  664. {
  665. // The door failed to close (blocked?). Try again in one second.
  666. door.NextStateChange = time + TimeSpan.FromSeconds(1);
  667. }
  668. break;
  669. case DoorState.Welded:
  670. // A welded door? This should never have been active in the first place.
  671. Log.Error($"Welded door was in the list of active doors. Door: {ToPrettyString(ent)}");
  672. break;
  673. }
  674. }
  675. #endregion
  676. }