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
}
}