using Content.Shared.ActionBlocker; using Content.Shared.Administration.Logs; using Content.Shared.Climbing.Systems; using Content.Shared.Containers; using Content.Shared.Database; using Content.Shared.Disposal.Components; using Content.Shared.Disposal.Holder; using Content.Shared.Disposal.Tube; using Content.Shared.DoAfter; using Content.Shared.DragDrop; using Content.Shared.Emag.Systems; using Content.Shared.Explosion; using Content.Shared.Hands.EntitySystems; using Content.Shared.Interaction; using Content.Shared.Item; using Content.Shared.Movement.Events; using Content.Shared.Popups; using Content.Shared.Power; using Content.Shared.Power.EntitySystems; using Content.Shared.Storage.Components; using Content.Shared.Throwing; using Content.Shared.Verbs; using Content.Shared.Whitelist; using Robust.Shared.Audio.Systems; using Robust.Shared.Containers; using Robust.Shared.Map.Components; using Robust.Shared.Network; using Robust.Shared.Physics.Events; using Robust.Shared.Physics.Systems; using Robust.Shared.Timing; using System.Linq; namespace Content.Shared.Disposal.Unit; /// /// This system handles all operations relating to disposal units. /// public abstract partial class SharedDisposalUnitSystem : EntitySystem { [Dependency] private ActionBlockerSystem _actionBlocker = default!; [Dependency] private EntityWhitelistSystem _whitelist = default!; [Dependency] private SharedAppearanceSystem _appearance = default!; [Dependency] private SharedAudioSystem _audio = default!; [Dependency] private IGameTiming _timing = default!; [Dependency] private ISharedAdminLogManager _adminLog = default!; [Dependency] private ClimbSystem _climb = default!; [Dependency] private SharedContainerSystem _container = default!; [Dependency] private SharedJointSystem _joints = default!; [Dependency] private SharedPowerReceiverSystem _power = default!; [Dependency] private SharedDisposalHolderSystem _disposalHolder = default!; [Dependency] private SharedPopupSystem _popup = default!; [Dependency] private SharedDoAfterSystem _doAfter = default!; [Dependency] private SharedHandsSystem _hands = default!; [Dependency] private SharedUserInterfaceSystem _ui = default!; [Dependency] private SharedMapSystem _map = default!; [Dependency] private SharedTransformSystem _xform = default!; [Dependency] private INetManager _net = default!; [Dependency] private SharedPowerStateSystem _powerState = default!; public override void Initialize() { base.Initialize(); SubscribeLocalEvent(OnComponentInit); SubscribeLocalEvent(OnExploded); SubscribeLocalEvent(OnPowerChange); SubscribeLocalEvent(OnAnchorChanged); SubscribeLocalEvent(OnPreventCollide); SubscribeLocalEvent(OnEmagged); // See SharedDisposalUnitSystem.Interactions SubscribeLocalEvent>(AddInteractionVerb); SubscribeLocalEvent>(AddAltVerbs); SubscribeLocalEvent>(AddEnterOrExitVerb); SubscribeLocalEvent(OnDoAfter); SubscribeLocalEvent(OnThrowInsert); SubscribeLocalEvent(OnInsertAttempt); SubscribeLocalEvent(OnActivate); SubscribeLocalEvent(OnAfterInteractUsing); SubscribeLocalEvent(OnMovement); SubscribeLocalEvent(OnCanDragDropOn); SubscribeLocalEvent(OnDragDropOn); SubscribeLocalEvent(OnGetDumpableVerb); SubscribeLocalEvent(OnDump); // See SharedDisposalUnitSystem.Visuals SubscribeLocalEvent(OnUiButtonPressed); } #region: Event handling protected virtual void OnComponentInit(Entity ent, ref ComponentInit args) { ent.Comp.Container = _container.EnsureContainer(ent, nameof(DisposalUnitComponent)); } private void OnExploded(Entity ent, ref BeforeExplodeEvent args) { args.Contents.AddRange(GetContainedEntities(ent)); } private void OnPowerChange(Entity ent, ref PowerChangedEvent args) { RecalculateFlushTime(ent, true); UpdateVisualState(ent); } private void OnAnchorChanged(Entity ent, ref AnchorStateChangedEvent args) { if (args.Anchored) return; EjectContents(ent); } protected void OnPreventCollide(Entity ent, ref PreventCollideEvent args) { var otherBody = args.OtherEntity; // Items dropped shouldn't collide but items thrown should if (HasComp(otherBody) && !HasComp(otherBody)) { args.Cancelled = true; } } protected void OnEmagged(Entity ent, ref GotEmaggedEvent args) { ent.Comp.DisablePressure = true; args.Handled = true; } private void OnGetDumpableVerb(Entity ent, ref GetDumpableVerbEvent args) { args.Verb = Loc.GetString("dump-disposal-verb-name", ("unit", ent)); } private void OnDump(Entity ent, ref DumpEvent args) { if (args.Handled) return; args.Handled = true; args.PlaySound = true; foreach (var entity in args.DumpQueue) { Insert(ent, entity, args.User); } } #endregion /// /// Checks whether a disposal unit can flush. /// /// The disposal unit. /// True if the disposal unit can flush. public bool CanFlush(Entity ent) { return GetState(ent) == DisposalsPressureState.Ready && _power.IsPowered(ent.Owner) && Transform(ent).Anchored; } /// /// Gets the current pressure state of a disposals unit. /// /// The disposal unit. /// The disposal unit's pressure state. public DisposalsPressureState GetState(Entity ent) { var nextPressure = ent.Comp.NextPressurized - _timing.CurTime; var pressurizeTime = 1f / ent.Comp.PressurePerSecond; var pressurizeDuration = pressurizeTime - ent.Comp.FlushDelay.TotalSeconds; if (nextPressure.TotalSeconds > pressurizeDuration) return DisposalsPressureState.Flushed; if (nextPressure > TimeSpan.Zero) return DisposalsPressureState.Pressurizing; return DisposalsPressureState.Ready; } /// /// Handles the actual insertion of an entity into a disposal unit. /// /// The disposal unit. /// The entity inserted. /// The one who inserted the entity. /// Do the insertion now. protected void Insert(Entity ent, EntityUid inserted, EntityUid? user = null) { if (ent.Comp.Container != null) { // Note: the entity may have been inserted via SharedHandsSystem already _container.Insert(inserted, ent.Comp.Container); } if (!GetContainedEntities(ent).Contains(inserted)) return; if (_timing.CurTime >= ent.Comp.NextAllowedInsertSound) { _audio.PlayPredicted(ent.Comp.InsertSound, ent, user: user); ent.Comp.NextAllowedInsertSound = _timing.CurTime + ent.Comp.InsertSoundDelay; } if (user != inserted && user != null) { _adminLog.Add(LogType.Action, LogImpact.Medium, $"{ToPrettyString(user.Value):player} inserted {ToPrettyString(inserted)} into {ToPrettyString(ent)}"); } AutomaticEngage(ent); _joints.RecursiveClearJoints(inserted); _ui.CloseUi(ent.Owner, DisposalUnitUiKey.Key, inserted); UpdateVisualState(ent); } public override void Update(float frameTime) { base.Update(frameTime); var query = EntityQueryEnumerator(); while (query.MoveNext(out var uid, out var unit, out var metadata)) { UpdateDisposalUnit((uid, unit), metadata); } } private void UpdateDisposalUnit(Entity ent, MetaDataComponent metadata) { var state = GetState(ent); // Check if we need a state update if (ent.Comp.NextPressurized > _timing.CurTime) { UpdateState(ent, state); return; } // Check if we need to flush if (ent.Comp.NextFlush != null && ent.Comp.NextFlush.Value < _timing.CurTime) { TryFlush(ent); } UpdateState(ent, state); } private void UpdateState(Entity ent, DisposalsPressureState state) { if (ent.Comp.State == state) return; ent.Comp.State = state; switch (state) { case DisposalsPressureState.Ready: ent.Comp.NextPressurized = TimeSpan.Zero; _powerState.SetWorkingState(ent.Owner, false); break; case DisposalsPressureState.Pressurizing: _powerState.SetWorkingState(ent.Owner, true); break; } RecalculateFlushTime(ent, true); UpdateVisualState(ent); } /// /// Try to flush a disposal unit. /// /// The disposal unit. /// True if the flush was successful. public bool TryFlush(Entity ent) { if (!CanFlush(ent)) return false; var beforeFlushArgs = new BeforeDisposalFlushEvent(); RaiseLocalEvent(ent, beforeFlushArgs); if (beforeFlushArgs.Cancelled) { SetEngage(ent, false); return false; } var xform = Transform(ent); if (!TryComp(xform.GridUid, out MapGridComponent? grid)) return false; // Try to find an entry into disposals Entity? tube = null; foreach (var tubeUid in _map.GetLocal(xform.GridUid.Value, grid, xform.Coordinates)) { if (HasComp(tubeUid) && TryComp(tubeUid, out var tubeComp)) { tube = new Entity(tubeUid, tubeComp); break; } } if (tube == null) { SetEngage(ent, false); return false; } // Try to transfer entities from the unit into disposals. TryTransfer(ent, tube.Value, beforeFlushArgs.Tags); ent.Comp.NextPressurized = _timing.CurTime; if (!ent.Comp.DisablePressure) { ent.Comp.NextPressurized += TimeSpan.FromSeconds(1f / ent.Comp.PressurePerSecond); } SetEngage(ent, false); return true; } /// /// Tries to transfer all entities in a disposal unit to the disposals system. /// /// The disposal unit. /// The tube leading into disposals. /// True if the transfer was successful. public bool TryTransfer(Entity ent, Entity tube, IEnumerable? tags = null) { if (_net.IsClient && !_timing.IsFirstTimePredicted) return false; if (GetContainedEntityCount(ent) == 0) return false; var xform = Transform(ent); var holderUid = Spawn(ent.Comp.HolderPrototypeId, _xform.GetMapCoordinates(ent, xform: xform)); var holder = new Entity(holderUid, Comp(holderUid)); if (holder.Comp.Container == null) return false; foreach (var entity in GetContainedEntities(ent)) { _container.Insert(entity, holder.Comp.Container); _disposalHolder.AttachEntity((holderUid, holder), entity); } if (tags != null) { foreach (var tag in tags) { _disposalHolder.AddTag(holder, tag); } Dirty(holder); } // Take in the local atmos and pass it to the spawned disposals holder. IntakeAir(ent, xform); _disposalHolder.TransferAtmos(holder, ent); return _disposalHolder.TryEnterTube(holder, (tube, tube.Comp)); } /// /// Remove all entities currently in a disposal unit. /// /// The disposal unit. public void EjectContents(Entity ent) { foreach (var toRemove in GetContainedEntities(ent)) { Remove(ent, toRemove); } UpdateVisualState(ent); } /// /// Remove an entity from a disposal unit. /// /// The disposal unit. /// The entity to remove. public void Remove(Entity ent, EntityUid toRemove) { if (_timing.ApplyingState) return; if (!Terminating(toRemove) && ent.Comp.Container != null && _container.Remove(toRemove, ent.Comp.Container)) { _climb.Climb(toRemove, toRemove, ent, silent: true); } RecalculateFlushTime(ent); UpdateVisualState(ent); } /// /// Recalculates when a disposal will flush next /// /// The disposal unit. /// Reset any in-progress flushes. public void RecalculateFlushTime(Entity ent, bool reset = false) { TimeSpan? nextFlush = null; // Reset next flush? if (reset) { ent.Comp.NextFlush = null; } // If engaged/automatically engaged, figure out the next flush time if (ent.Comp.Engaged || GetContainedEntityCount(ent) > 0) { var newFlush = ent.Comp.Engaged ? _timing.CurTime + ent.Comp.ManualFlushTime : _timing.CurTime + ent.Comp.AutomaticEngageTime; if (GetState(ent) != DisposalsPressureState.Ready) { newFlush += ent.Comp.NextPressurized; } nextFlush = (ent.Comp.NextFlush ?? TimeSpan.MaxValue); // Check if there is a flush in progress that will occur // sooner than what was calculated nextFlush = TimeSpan.FromSeconds(Math.Min(nextFlush.Value.TotalSeconds, newFlush.TotalSeconds)); } ent.Comp.NextFlush = nextFlush; Dirty(ent); UpdateUI(ent); } /// /// Sets a disposal unit to move towards flushing itself. /// /// The disposal unit. /// The disposal unit's metadata. public void SetEngage(Entity ent, bool engaged) { ent.Comp.Engaged = engaged; RecalculateFlushTime(ent); UpdateVisualState(ent); } /// /// Primes a disposal unit to automatically flush sometime in the future. /// /// The disposal unit. /// The disposal unit's metadata. public void AutomaticEngage(Entity ent) { if (!ent.Comp.AutomaticEngage) return; RecalculateFlushTime(ent); } /// /// Toggles a disposal unit between 'engaged' and 'disengaged'. /// /// The disposal unit. public void ToggleEngage(Entity ent) { SetEngage(ent, !ent.Comp.Engaged); } /// /// All entities contained in a disposal unit. /// /// The disposal unit. /// A copy of the disposal unit's ContainedEntities list. public IReadOnlyList GetContainedEntities(Entity ent) { if (ent.Comp.Container == null) return new List(); return ent.Comp.Container.ContainedEntities.ToList(); } /// /// The number of entities contained in a disposal unit. /// /// The disposal unit. /// The entity count. public int GetContainedEntityCount(Entity ent) { return GetContainedEntities(ent).Count; } /// /// Takes the atmos surrounding the disposal unit into itself. /// /// The disposal unit. /// The disposal unit's transform. protected virtual void IntakeAir(Entity ent, TransformComponent xform) { // Handled by the server } }