DisposalUnitSystem.cs 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823
  1. using System.Diagnostics.CodeAnalysis;
  2. using System.Linq;
  3. using Content.Server.Administration.Logs;
  4. using Content.Server.Atmos.EntitySystems;
  5. using Content.Server.Containers;
  6. using Content.Server.Disposal.Tube;
  7. using Content.Server.Disposal.Tube.Components;
  8. using Content.Server.Disposal.Unit.Components;
  9. using Content.Server.Popups;
  10. using Content.Server.Power.Components;
  11. using Content.Server.Power.EntitySystems;
  12. using Content.Shared.ActionBlocker;
  13. using Content.Shared.Atmos;
  14. using Content.Shared.Database;
  15. using Content.Shared.Destructible;
  16. using Content.Shared.Disposal;
  17. using Content.Shared.Disposal.Components;
  18. using Content.Shared.DoAfter;
  19. using Content.Shared.DragDrop;
  20. using Content.Shared.Emag.Systems;
  21. using Content.Shared.Explosion;
  22. using Content.Shared.Hands.Components;
  23. using Content.Shared.Hands.EntitySystems;
  24. using Content.Shared.IdentityManagement;
  25. using Content.Shared.Interaction;
  26. using Content.Shared.Item;
  27. using Content.Shared.Movement.Events;
  28. using Content.Shared.Popups;
  29. using Content.Shared.Power;
  30. using Content.Shared.Verbs;
  31. using Robust.Server.Audio;
  32. using Robust.Server.GameObjects;
  33. using Robust.Shared.Containers;
  34. using Robust.Shared.GameStates;
  35. using Robust.Shared.Map.Components;
  36. using Robust.Shared.Physics.Components;
  37. using Robust.Shared.Physics.Events;
  38. using Robust.Shared.Player;
  39. using Robust.Shared.Utility;
  40. namespace Content.Server.Disposal.Unit.EntitySystems;
  41. public sealed class DisposalUnitSystem : SharedDisposalUnitSystem
  42. {
  43. [Dependency] private readonly IAdminLogManager _adminLogger = default!;
  44. [Dependency] private readonly ActionBlockerSystem _actionBlockerSystem = default!;
  45. [Dependency] private readonly AppearanceSystem _appearance = default!;
  46. [Dependency] private readonly AtmosphereSystem _atmosSystem = default!;
  47. [Dependency] private readonly AudioSystem _audioSystem = default!;
  48. [Dependency] private readonly DisposalTubeSystem _disposalTubeSystem = default!;
  49. [Dependency] private readonly EntityLookupSystem _lookup = default!;
  50. [Dependency] private readonly PopupSystem _popupSystem = default!;
  51. [Dependency] private readonly PowerReceiverSystem _power = default!;
  52. [Dependency] private readonly SharedContainerSystem _containerSystem = default!;
  53. [Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!;
  54. [Dependency] private readonly SharedHandsSystem _handsSystem = default!;
  55. [Dependency] private readonly TransformSystem _transformSystem = default!;
  56. [Dependency] private readonly UserInterfaceSystem _ui = default!;
  57. [Dependency] private readonly SharedMapSystem _map = default!;
  58. public override void Initialize()
  59. {
  60. base.Initialize();
  61. SubscribeLocalEvent<DisposalUnitComponent, ComponentGetState>(OnGetState);
  62. SubscribeLocalEvent<DisposalUnitComponent, PreventCollideEvent>(OnPreventCollide);
  63. SubscribeLocalEvent<DisposalUnitComponent, CanDropTargetEvent>(OnCanDragDropOn);
  64. SubscribeLocalEvent<DisposalUnitComponent, GotEmaggedEvent>(OnEmagged);
  65. // Shouldn't need re-anchoring.
  66. SubscribeLocalEvent<DisposalUnitComponent, AnchorStateChangedEvent>(OnAnchorChanged);
  67. // TODO: Predict me when hands predicted
  68. SubscribeLocalEvent<DisposalUnitComponent, ContainerRelayMovementEntityEvent>(OnMovement);
  69. SubscribeLocalEvent<DisposalUnitComponent, PowerChangedEvent>(OnPowerChange);
  70. SubscribeLocalEvent<DisposalUnitComponent, ComponentInit>(OnDisposalInit);
  71. SubscribeLocalEvent<DisposalUnitComponent, ActivateInWorldEvent>(OnActivate);
  72. SubscribeLocalEvent<DisposalUnitComponent, AfterInteractUsingEvent>(OnAfterInteractUsing);
  73. SubscribeLocalEvent<DisposalUnitComponent, DragDropTargetEvent>(OnDragDropOn);
  74. SubscribeLocalEvent<DisposalUnitComponent, DestructionEventArgs>(OnDestruction);
  75. SubscribeLocalEvent<DisposalUnitComponent, BeforeExplodeEvent>(OnExploded);
  76. SubscribeLocalEvent<DisposalUnitComponent, GetVerbsEvent<InteractionVerb>>(AddInsertVerb);
  77. SubscribeLocalEvent<DisposalUnitComponent, GetVerbsEvent<AlternativeVerb>>(AddDisposalAltVerbs);
  78. SubscribeLocalEvent<DisposalUnitComponent, GetVerbsEvent<Verb>>(AddClimbInsideVerb);
  79. SubscribeLocalEvent<DisposalUnitComponent, DisposalDoAfterEvent>(OnDoAfter);
  80. SubscribeLocalEvent<DisposalUnitComponent, BeforeThrowInsertEvent>(OnThrowInsert);
  81. SubscribeLocalEvent<DisposalUnitComponent, SharedDisposalUnitComponent.UiButtonPressedMessage>(OnUiButtonPressed);
  82. }
  83. private void OnGetState(EntityUid uid, DisposalUnitComponent component, ref ComponentGetState args)
  84. {
  85. args.State = new DisposalUnitComponentState(
  86. component.FlushSound,
  87. component.State,
  88. component.NextPressurized,
  89. component.AutomaticEngageTime,
  90. component.NextFlush,
  91. component.Powered,
  92. component.Engaged,
  93. GetNetEntityList(component.RecentlyEjected));
  94. }
  95. private void AddDisposalAltVerbs(EntityUid uid, SharedDisposalUnitComponent component, GetVerbsEvent<AlternativeVerb> args)
  96. {
  97. if (!args.CanAccess || !args.CanInteract)
  98. return;
  99. // Behavior for if the disposals bin has items in it
  100. if (component.Container.ContainedEntities.Count > 0)
  101. {
  102. // Verbs to flush the unit
  103. AlternativeVerb flushVerb = new()
  104. {
  105. Act = () => ManualEngage(uid, component),
  106. Text = Loc.GetString("disposal-flush-verb-get-data-text"),
  107. Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/delete_transparent.svg.192dpi.png")),
  108. Priority = 1,
  109. };
  110. args.Verbs.Add(flushVerb);
  111. // Verb to eject the contents
  112. AlternativeVerb ejectVerb = new()
  113. {
  114. Act = () => TryEjectContents(uid, component),
  115. Category = VerbCategory.Eject,
  116. Text = Loc.GetString("disposal-eject-verb-get-data-text")
  117. };
  118. args.Verbs.Add(ejectVerb);
  119. }
  120. }
  121. private void AddClimbInsideVerb(EntityUid uid, SharedDisposalUnitComponent component, GetVerbsEvent<Verb> args)
  122. {
  123. // This is not an interaction, activation, or alternative verb type because unfortunately most users are
  124. // unwilling to accept that this is where they belong and don't want to accidentally climb inside.
  125. if (!args.CanAccess ||
  126. !args.CanInteract ||
  127. component.Container.ContainedEntities.Contains(args.User) ||
  128. !_actionBlockerSystem.CanMove(args.User))
  129. {
  130. return;
  131. }
  132. if (!CanInsert(uid, component, args.User))
  133. return;
  134. // Add verb to climb inside of the unit,
  135. Verb verb = new()
  136. {
  137. Act = () => TryInsert(uid, args.User, args.User),
  138. DoContactInteraction = true,
  139. Text = Loc.GetString("disposal-self-insert-verb-get-data-text")
  140. };
  141. // TODO VERB ICON
  142. // TODO VERB CATEGORY
  143. // create a verb category for "enter"?
  144. // See also, medical scanner. Also maybe add verbs for entering lockers/body bags?
  145. args.Verbs.Add(verb);
  146. }
  147. private void AddInsertVerb(EntityUid uid, SharedDisposalUnitComponent component, GetVerbsEvent<InteractionVerb> args)
  148. {
  149. if (!args.CanAccess || !args.CanInteract || args.Hands == null || args.Using == null)
  150. return;
  151. if (!_actionBlockerSystem.CanDrop(args.User))
  152. return;
  153. if (!CanInsert(uid, component, args.Using.Value))
  154. return;
  155. InteractionVerb insertVerb = new()
  156. {
  157. Text = Name(args.Using.Value),
  158. Category = VerbCategory.Insert,
  159. Act = () =>
  160. {
  161. _handsSystem.TryDropIntoContainer(args.User, args.Using.Value, component.Container, checkActionBlocker: false, args.Hands);
  162. _adminLogger.Add(LogType.Action, LogImpact.Medium, $"{ToPrettyString(args.User):player} inserted {ToPrettyString(args.Using.Value)} into {ToPrettyString(uid)}");
  163. AfterInsert(uid, component, args.Using.Value, args.User);
  164. }
  165. };
  166. args.Verbs.Add(insertVerb);
  167. }
  168. private void OnDoAfter(EntityUid uid, SharedDisposalUnitComponent component, DoAfterEvent args)
  169. {
  170. if (args.Handled || args.Cancelled || args.Args.Target == null || args.Args.Used == null)
  171. return;
  172. AfterInsert(uid, component, args.Args.Target.Value, args.Args.User, doInsert: true);
  173. args.Handled = true;
  174. }
  175. private void OnThrowInsert(Entity<DisposalUnitComponent> ent, ref BeforeThrowInsertEvent args)
  176. {
  177. if (!CanInsert(ent, ent, args.ThrownEntity))
  178. args.Cancelled = true;
  179. }
  180. public override void DoInsertDisposalUnit(EntityUid uid, EntityUid toInsert, EntityUid user, SharedDisposalUnitComponent? disposal = null)
  181. {
  182. if (!ResolveDisposals(uid, ref disposal))
  183. return;
  184. if (!_containerSystem.Insert(toInsert, disposal.Container))
  185. return;
  186. _adminLogger.Add(LogType.Action, LogImpact.Medium, $"{ToPrettyString(user):player} inserted {ToPrettyString(toInsert)} into {ToPrettyString(uid)}");
  187. AfterInsert(uid, disposal, toInsert, user);
  188. }
  189. public override void Update(float frameTime)
  190. {
  191. base.Update(frameTime);
  192. var query = AllEntityQuery<DisposalUnitComponent, MetaDataComponent>();
  193. while (query.MoveNext(out var uid, out var unit, out var metadata))
  194. {
  195. if (!metadata.EntityPaused)
  196. Update(uid, unit, metadata, frameTime);
  197. }
  198. }
  199. #region UI Handlers
  200. private void OnUiButtonPressed(EntityUid uid, SharedDisposalUnitComponent component, SharedDisposalUnitComponent.UiButtonPressedMessage args)
  201. {
  202. if (args.Actor is not { Valid: true } player)
  203. {
  204. return;
  205. }
  206. switch (args.Button)
  207. {
  208. case SharedDisposalUnitComponent.UiButton.Eject:
  209. TryEjectContents(uid, component);
  210. _adminLogger.Add(LogType.Action, LogImpact.Low, $"{ToPrettyString(player):player} hit eject button on {ToPrettyString(uid)}");
  211. break;
  212. case SharedDisposalUnitComponent.UiButton.Engage:
  213. ToggleEngage(uid, component);
  214. _adminLogger.Add(LogType.Action, LogImpact.Low, $"{ToPrettyString(player):player} hit flush button on {ToPrettyString(uid)}, it's now {(component.Engaged ? "on" : "off")}");
  215. break;
  216. case SharedDisposalUnitComponent.UiButton.Power:
  217. _power.TogglePower(uid, user: args.Actor);
  218. break;
  219. default:
  220. throw new ArgumentOutOfRangeException($"{ToPrettyString(player):player} attempted to hit a nonexistant button on {ToPrettyString(uid)}");
  221. }
  222. }
  223. public void ToggleEngage(EntityUid uid, SharedDisposalUnitComponent component)
  224. {
  225. component.Engaged ^= true;
  226. if (component.Engaged)
  227. {
  228. ManualEngage(uid, component);
  229. }
  230. else
  231. {
  232. Disengage(uid, component);
  233. }
  234. }
  235. #endregion
  236. #region Eventbus Handlers
  237. private void OnActivate(EntityUid uid, SharedDisposalUnitComponent component, ActivateInWorldEvent args)
  238. {
  239. if (args.Handled || !args.Complex)
  240. return;
  241. if (!TryComp(args.User, out ActorComponent? actor))
  242. {
  243. return;
  244. }
  245. args.Handled = true;
  246. _ui.OpenUi(uid, SharedDisposalUnitComponent.DisposalUnitUiKey.Key, actor.PlayerSession);
  247. }
  248. private void OnAfterInteractUsing(EntityUid uid, SharedDisposalUnitComponent component, AfterInteractUsingEvent args)
  249. {
  250. if (args.Handled || !args.CanReach)
  251. return;
  252. if (!HasComp<HandsComponent>(args.User))
  253. {
  254. return;
  255. }
  256. if (!CanInsert(uid, component, args.Used) || !_handsSystem.TryDropIntoContainer(args.User, args.Used, component.Container))
  257. {
  258. return;
  259. }
  260. _adminLogger.Add(LogType.Action, LogImpact.Medium, $"{ToPrettyString(args.User):player} inserted {ToPrettyString(args.Used)} into {ToPrettyString(uid)}");
  261. AfterInsert(uid, component, args.Used, args.User);
  262. args.Handled = true;
  263. }
  264. private void OnDisposalInit(EntityUid uid, SharedDisposalUnitComponent component, ComponentInit args)
  265. {
  266. component.Container = _containerSystem.EnsureContainer<Container>(uid, SharedDisposalUnitComponent.ContainerId);
  267. UpdateInterface(uid, component, component.Powered);
  268. }
  269. private void OnPowerChange(EntityUid uid, SharedDisposalUnitComponent component, ref PowerChangedEvent args)
  270. {
  271. if (!component.Running || args.Powered == component.Powered)
  272. return;
  273. component.Powered = args.Powered;
  274. UpdateVisualState(uid, component);
  275. UpdateInterface(uid, component, args.Powered);
  276. if (!args.Powered)
  277. {
  278. component.NextFlush = null;
  279. Dirty(uid, component);
  280. return;
  281. }
  282. if (component.Engaged)
  283. {
  284. // Run ManualEngage to recalculate a new flush time
  285. ManualEngage(uid, component);
  286. }
  287. }
  288. // TODO: This should just use the same thing as entity storage?
  289. private void OnMovement(EntityUid uid, SharedDisposalUnitComponent component, ref ContainerRelayMovementEntityEvent args)
  290. {
  291. var currentTime = GameTiming.CurTime;
  292. if (!_actionBlockerSystem.CanMove(args.Entity))
  293. return;
  294. if (!TryComp(args.Entity, out HandsComponent? hands) ||
  295. hands.Count == 0 ||
  296. currentTime < component.LastExitAttempt + ExitAttemptDelay)
  297. return;
  298. component.LastExitAttempt = currentTime;
  299. Remove(uid, component, args.Entity);
  300. }
  301. private void OnAnchorChanged(EntityUid uid, SharedDisposalUnitComponent component, ref AnchorStateChangedEvent args)
  302. {
  303. if (Terminating(uid))
  304. return;
  305. UpdateVisualState(uid, component);
  306. if (!args.Anchored)
  307. TryEjectContents(uid, component);
  308. }
  309. private void OnDestruction(EntityUid uid, SharedDisposalUnitComponent component, DestructionEventArgs args)
  310. {
  311. TryEjectContents(uid, component);
  312. }
  313. private void OnDragDropOn(EntityUid uid, SharedDisposalUnitComponent component, ref DragDropTargetEvent args)
  314. {
  315. args.Handled = TryInsert(uid, args.Dragged, args.User);
  316. }
  317. #endregion
  318. private void UpdateState(EntityUid uid, DisposalsPressureState state, SharedDisposalUnitComponent component, MetaDataComponent metadata)
  319. {
  320. if (component.State == state)
  321. return;
  322. component.State = state;
  323. UpdateVisualState(uid, component);
  324. UpdateInterface(uid, component, component.Powered);
  325. Dirty(uid, component, metadata);
  326. if (state == DisposalsPressureState.Ready)
  327. {
  328. component.NextPressurized = TimeSpan.Zero;
  329. // Manually engaged
  330. if (component.Engaged)
  331. {
  332. component.NextFlush = GameTiming.CurTime + component.ManualFlushTime;
  333. }
  334. else if (component.Container.ContainedEntities.Count > 0)
  335. {
  336. component.NextFlush = GameTiming.CurTime + component.AutomaticEngageTime;
  337. }
  338. else
  339. {
  340. component.NextFlush = null;
  341. }
  342. }
  343. }
  344. /// <summary>
  345. /// Work out if we can stop updating this disposals component i.e. full pressure and nothing colliding.
  346. /// </summary>
  347. private void Update(EntityUid uid, SharedDisposalUnitComponent component, MetaDataComponent metadata, float frameTime)
  348. {
  349. var state = GetState(uid, component, metadata);
  350. // Pressurizing, just check if we need a state update.
  351. if (component.NextPressurized > GameTiming.CurTime)
  352. {
  353. UpdateState(uid, state, component, metadata);
  354. return;
  355. }
  356. if (component.NextFlush != null)
  357. {
  358. if (component.NextFlush.Value < GameTiming.CurTime)
  359. {
  360. TryFlush(uid, component);
  361. }
  362. }
  363. UpdateState(uid, state, component, metadata);
  364. Box2? disposalsBounds = null;
  365. var count = component.RecentlyEjected.Count;
  366. if (count > 0)
  367. {
  368. if (!HasComp<PhysicsComponent>(uid))
  369. {
  370. component.RecentlyEjected.Clear();
  371. }
  372. else
  373. {
  374. disposalsBounds = _lookup.GetWorldAABB(uid);
  375. }
  376. }
  377. for (var i = 0; i < component.RecentlyEjected.Count; i++)
  378. {
  379. var ejectedId = component.RecentlyEjected[i];
  380. if (HasComp<PhysicsComponent>(ejectedId))
  381. {
  382. // TODO: We need to use a specific collision method (which sloth hasn't coded yet) for actual bounds overlaps.
  383. // TODO: Come do this sloth :^)
  384. // Check for itemcomp as we won't just block the disposal unit "sleeping" for something it can't collide with anyway.
  385. if (!HasComp<ItemComponent>(ejectedId)
  386. && _lookup.GetWorldAABB(ejectedId).Intersects(disposalsBounds!.Value))
  387. {
  388. continue;
  389. }
  390. component.RecentlyEjected.RemoveAt(i);
  391. i--;
  392. }
  393. }
  394. if (count != component.RecentlyEjected.Count)
  395. Dirty(uid, component, metadata);
  396. }
  397. public bool TryInsert(EntityUid unitId, EntityUid toInsertId, EntityUid? userId, DisposalUnitComponent? unit = null)
  398. {
  399. if (!Resolve(unitId, ref unit))
  400. return false;
  401. if (userId.HasValue && !HasComp<HandsComponent>(userId) && toInsertId != userId) // Mobs like mouse can Jump inside even with no hands
  402. {
  403. _popupSystem.PopupEntity(Loc.GetString("disposal-unit-no-hands"), userId.Value, userId.Value, PopupType.SmallCaution);
  404. return false;
  405. }
  406. if (!CanInsert(unitId, unit, toInsertId))
  407. return false;
  408. bool insertingSelf = userId == toInsertId;
  409. var delay = insertingSelf ? unit.EntryDelay : unit.DraggedEntryDelay;
  410. if (userId != null && !insertingSelf)
  411. _popupSystem.PopupEntity(Loc.GetString("disposal-unit-being-inserted", ("user", Identity.Entity((EntityUid)userId, EntityManager))), toInsertId, toInsertId, PopupType.Large);
  412. if (delay <= 0 || userId == null)
  413. {
  414. AfterInsert(unitId, unit, toInsertId, userId, doInsert: true);
  415. return true;
  416. }
  417. // Can't check if our target AND disposals moves currently so we'll just check target.
  418. // if you really want to check if disposals moves then add a predicate.
  419. var doAfterArgs = new DoAfterArgs(EntityManager, userId.Value, delay, new DisposalDoAfterEvent(), unitId, target: toInsertId, used: unitId)
  420. {
  421. BreakOnDamage = true,
  422. BreakOnMove = true,
  423. NeedHand = false,
  424. };
  425. _doAfterSystem.TryStartDoAfter(doAfterArgs);
  426. return true;
  427. }
  428. public bool TryFlush(EntityUid uid, SharedDisposalUnitComponent component)
  429. {
  430. if (!CanFlush(uid, component))
  431. {
  432. return false;
  433. }
  434. if (component.NextFlush != null)
  435. component.NextFlush = component.NextFlush.Value + component.AutomaticEngageTime;
  436. var beforeFlushArgs = new BeforeDisposalFlushEvent();
  437. RaiseLocalEvent(uid, beforeFlushArgs);
  438. if (beforeFlushArgs.Cancelled)
  439. {
  440. Disengage(uid, component);
  441. return false;
  442. }
  443. var xform = Transform(uid);
  444. if (!TryComp(xform.GridUid, out MapGridComponent? grid))
  445. return false;
  446. var coords = xform.Coordinates;
  447. var entry = _map.GetLocal(xform.GridUid.Value, grid, coords)
  448. .FirstOrDefault(HasComp<DisposalEntryComponent>);
  449. if (entry == default || component is not DisposalUnitComponent sDisposals)
  450. {
  451. component.Engaged = false;
  452. Dirty(uid, component);
  453. return false;
  454. }
  455. HandleAir(uid, sDisposals, xform);
  456. _disposalTubeSystem.TryInsert(entry, sDisposals, beforeFlushArgs.Tags);
  457. component.NextPressurized = GameTiming.CurTime;
  458. if (!component.DisablePressure)
  459. component.NextPressurized += TimeSpan.FromSeconds(1f / PressurePerSecond);
  460. component.Engaged = false;
  461. // stop queuing NOW
  462. component.NextFlush = null;
  463. UpdateVisualState(uid, component, true);
  464. UpdateInterface(uid, component, component.Powered);
  465. Dirty(uid, component);
  466. return true;
  467. }
  468. private void HandleAir(EntityUid uid, DisposalUnitComponent component, TransformComponent xform)
  469. {
  470. var air = component.Air;
  471. var indices = _transformSystem.GetGridTilePositionOrDefault((uid, xform));
  472. if (_atmosSystem.GetTileMixture(xform.GridUid, xform.MapUid, indices, true) is { Temperature: > 0f } environment)
  473. {
  474. var transferMoles = 0.1f * (0.25f * Atmospherics.OneAtmosphere * 1.01f - air.Pressure) * air.Volume / (environment.Temperature * Atmospherics.R);
  475. component.Air = environment.Remove(transferMoles);
  476. }
  477. }
  478. public void UpdateInterface(EntityUid uid, SharedDisposalUnitComponent component, bool powered)
  479. {
  480. var compState = GetState(uid, component);
  481. var stateString = Loc.GetString($"disposal-unit-state-{compState}");
  482. var state = new SharedDisposalUnitComponent.DisposalUnitBoundUserInterfaceState(Name(uid), stateString, EstimatedFullPressure(uid, component), powered, component.Engaged);
  483. _ui.SetUiState(uid, SharedDisposalUnitComponent.DisposalUnitUiKey.Key, state);
  484. var stateUpdatedEvent = new DisposalUnitUIStateUpdatedEvent(state);
  485. RaiseLocalEvent(uid, stateUpdatedEvent);
  486. }
  487. /// <summary>
  488. /// Returns the estimated time when the disposal unit will be back to full pressure.
  489. /// </summary>
  490. private TimeSpan EstimatedFullPressure(EntityUid uid, SharedDisposalUnitComponent component)
  491. {
  492. if (component.NextPressurized < GameTiming.CurTime)
  493. return TimeSpan.Zero;
  494. return component.NextPressurized;
  495. }
  496. public void UpdateVisualState(EntityUid uid, SharedDisposalUnitComponent component, bool flush = false)
  497. {
  498. if (!TryComp(uid, out AppearanceComponent? appearance))
  499. {
  500. return;
  501. }
  502. if (!Transform(uid).Anchored)
  503. {
  504. _appearance.SetData(uid, SharedDisposalUnitComponent.Visuals.VisualState, SharedDisposalUnitComponent.VisualState.UnAnchored, appearance);
  505. _appearance.SetData(uid, SharedDisposalUnitComponent.Visuals.Handle, SharedDisposalUnitComponent.HandleState.Normal, appearance);
  506. _appearance.SetData(uid, SharedDisposalUnitComponent.Visuals.Light, SharedDisposalUnitComponent.LightStates.Off, appearance);
  507. return;
  508. }
  509. var state = GetState(uid, component);
  510. switch (state)
  511. {
  512. case DisposalsPressureState.Flushed:
  513. _appearance.SetData(uid, SharedDisposalUnitComponent.Visuals.VisualState, SharedDisposalUnitComponent.VisualState.OverlayFlushing, appearance);
  514. break;
  515. case DisposalsPressureState.Pressurizing:
  516. _appearance.SetData(uid, SharedDisposalUnitComponent.Visuals.VisualState, SharedDisposalUnitComponent.VisualState.OverlayCharging, appearance);
  517. break;
  518. case DisposalsPressureState.Ready:
  519. _appearance.SetData(uid, SharedDisposalUnitComponent.Visuals.VisualState, SharedDisposalUnitComponent.VisualState.Anchored, appearance);
  520. break;
  521. }
  522. _appearance.SetData(uid, SharedDisposalUnitComponent.Visuals.Handle, component.Engaged
  523. ? SharedDisposalUnitComponent.HandleState.Engaged
  524. : SharedDisposalUnitComponent.HandleState.Normal, appearance);
  525. if (!component.Powered)
  526. {
  527. _appearance.SetData(uid, SharedDisposalUnitComponent.Visuals.Light, SharedDisposalUnitComponent.LightStates.Off, appearance);
  528. return;
  529. }
  530. var lightState = SharedDisposalUnitComponent.LightStates.Off;
  531. if (component.Container.ContainedEntities.Count > 0)
  532. {
  533. lightState |= SharedDisposalUnitComponent.LightStates.Full;
  534. }
  535. if (state is DisposalsPressureState.Pressurizing or DisposalsPressureState.Flushed)
  536. {
  537. lightState |= SharedDisposalUnitComponent.LightStates.Charging;
  538. }
  539. else
  540. {
  541. lightState |= SharedDisposalUnitComponent.LightStates.Ready;
  542. }
  543. _appearance.SetData(uid, SharedDisposalUnitComponent.Visuals.Light, lightState, appearance);
  544. }
  545. public void Remove(EntityUid uid, SharedDisposalUnitComponent component, EntityUid toRemove)
  546. {
  547. _containerSystem.Remove(toRemove, component.Container);
  548. if (component.Container.ContainedEntities.Count == 0)
  549. {
  550. // If not manually engaged then reset the flushing entirely.
  551. if (!component.Engaged)
  552. {
  553. component.NextFlush = null;
  554. }
  555. }
  556. if (!component.RecentlyEjected.Contains(toRemove))
  557. component.RecentlyEjected.Add(toRemove);
  558. UpdateVisualState(uid, component);
  559. Dirty(uid, component);
  560. }
  561. public bool CanFlush(EntityUid unit, SharedDisposalUnitComponent component)
  562. {
  563. return GetState(unit, component) == DisposalsPressureState.Ready
  564. && component.Powered
  565. && Comp<TransformComponent>(unit).Anchored;
  566. }
  567. public void ManualEngage(EntityUid uid, SharedDisposalUnitComponent component, MetaDataComponent? metadata = null)
  568. {
  569. component.Engaged = true;
  570. UpdateVisualState(uid, component);
  571. UpdateInterface(uid, component, component.Powered);
  572. Dirty(uid, component);
  573. if (!CanFlush(uid, component))
  574. return;
  575. if (!Resolve(uid, ref metadata))
  576. return;
  577. var pauseTime = Metadata.GetPauseTime(uid, metadata);
  578. var nextEngage = GameTiming.CurTime - pauseTime + component.ManualFlushTime;
  579. component.NextFlush = TimeSpan.FromSeconds(Math.Min((component.NextFlush ?? TimeSpan.MaxValue).TotalSeconds, nextEngage.TotalSeconds));
  580. }
  581. public void Disengage(EntityUid uid, SharedDisposalUnitComponent component)
  582. {
  583. component.Engaged = false;
  584. if (component.Container.ContainedEntities.Count == 0)
  585. {
  586. component.NextFlush = null;
  587. }
  588. UpdateVisualState(uid, component);
  589. UpdateInterface(uid, component, component.Powered);
  590. Dirty(uid, component);
  591. }
  592. /// <summary>
  593. /// Remove all entities currently in the disposal unit.
  594. /// </summary>
  595. public void TryEjectContents(EntityUid uid, SharedDisposalUnitComponent component)
  596. {
  597. foreach (var entity in component.Container.ContainedEntities.ToArray())
  598. {
  599. Remove(uid, component, entity);
  600. }
  601. if (!component.Engaged)
  602. {
  603. component.NextFlush = null;
  604. Dirty(uid, component);
  605. }
  606. }
  607. public override bool HasDisposals(EntityUid? uid)
  608. {
  609. return HasComp<DisposalUnitComponent>(uid);
  610. }
  611. public override bool ResolveDisposals(EntityUid uid, [NotNullWhen(true)] ref SharedDisposalUnitComponent? component)
  612. {
  613. if (component != null)
  614. return true;
  615. TryComp<DisposalUnitComponent>(uid, out var storage);
  616. component = storage;
  617. return component != null;
  618. }
  619. public override bool CanInsert(EntityUid uid, SharedDisposalUnitComponent component, EntityUid entity)
  620. {
  621. if (!base.CanInsert(uid, component, entity))
  622. return false;
  623. return _containerSystem.CanInsert(entity, component.Container);
  624. }
  625. /// <summary>
  626. /// If something is inserted (or the likes) then we'll queue up an automatic flush in the future.
  627. /// </summary>
  628. public void QueueAutomaticEngage(EntityUid uid, SharedDisposalUnitComponent component, MetaDataComponent? metadata = null)
  629. {
  630. if (component.Deleted || !component.AutomaticEngage || !component.Powered && component.Container.ContainedEntities.Count == 0)
  631. {
  632. return;
  633. }
  634. var pauseTime = Metadata.GetPauseTime(uid, metadata);
  635. var automaticTime = GameTiming.CurTime + component.AutomaticEngageTime - pauseTime;
  636. var flushTime = TimeSpan.FromSeconds(Math.Min((component.NextFlush ?? TimeSpan.MaxValue).TotalSeconds, automaticTime.TotalSeconds));
  637. component.NextFlush = flushTime;
  638. Dirty(uid, component);
  639. }
  640. public void AfterInsert(EntityUid uid, SharedDisposalUnitComponent component, EntityUid inserted, EntityUid? user = null, bool doInsert = false)
  641. {
  642. _audioSystem.PlayPvs(component.InsertSound, uid);
  643. if (doInsert && !_containerSystem.Insert(inserted, component.Container))
  644. return;
  645. if (user != inserted && user != null)
  646. _adminLogger.Add(LogType.Action, LogImpact.Medium, $"{ToPrettyString(user.Value):player} inserted {ToPrettyString(inserted)} into {ToPrettyString(uid)}");
  647. QueueAutomaticEngage(uid, component);
  648. _ui.CloseUi(uid, SharedDisposalUnitComponent.DisposalUnitUiKey.Key, inserted);
  649. // Maybe do pullable instead? Eh still fine.
  650. Joints.RecursiveClearJoints(inserted);
  651. UpdateVisualState(uid, component);
  652. }
  653. private void OnExploded(Entity<DisposalUnitComponent> ent, ref BeforeExplodeEvent args)
  654. {
  655. args.Contents.AddRange(ent.Comp.Container.ContainedEntities);
  656. }
  657. }
  658. /// <summary>
  659. /// Sent before the disposal unit flushes it's contents.
  660. /// Allows adding tags for sorting and preventing the disposal unit from flushing.
  661. /// </summary>
  662. public sealed class DisposalUnitUIStateUpdatedEvent : EntityEventArgs
  663. {
  664. public SharedDisposalUnitComponent.DisposalUnitBoundUserInterfaceState State;
  665. public DisposalUnitUIStateUpdatedEvent(SharedDisposalUnitComponent.DisposalUnitBoundUserInterfaceState state)
  666. {
  667. State = state;
  668. }
  669. }
  670. /// <summary>
  671. /// Sent before the disposal unit flushes it's contents.
  672. /// Allows adding tags for sorting and preventing the disposal unit from flushing.
  673. /// </summary>
  674. public sealed class BeforeDisposalFlushEvent : CancellableEntityEventArgs
  675. {
  676. public readonly List<string> Tags = new();
  677. }