diff --git a/Content.Client/Disposal/Components/DisposalUnitComponent.cs b/Content.Client/Disposal/Components/DisposalUnitComponent.cs deleted file mode 100644 index aa51b58ff1..0000000000 --- a/Content.Client/Disposal/Components/DisposalUnitComponent.cs +++ /dev/null @@ -1,26 +0,0 @@ -using Content.Shared.Disposal.Components; -using Robust.Client.Animations; -using Robust.Shared.Audio; - -namespace Content.Client.Disposal.Components -{ - [RegisterComponent] - [ComponentReference(typeof(SharedDisposalUnitComponent))] - public sealed class DisposalUnitComponent : SharedDisposalUnitComponent - { - [DataField("flushSound")] - public readonly SoundSpecifier? FlushSound; - - public Animation FlushAnimation = default!; - - public DisposalUnitBoundUserInterfaceState? UiState; - - public override void HandleComponentState(ComponentState? curState, ComponentState? nextState) - { - base.HandleComponentState(curState, nextState); - if (curState is not DisposalUnitComponentState state) return; - - RecentlyEjected = state.RecentlyEjected; - } - } -} diff --git a/Content.Client/Disposal/DisposalUnitComponent.cs b/Content.Client/Disposal/DisposalUnitComponent.cs new file mode 100644 index 0000000000..1ca18bedeb --- /dev/null +++ b/Content.Client/Disposal/DisposalUnitComponent.cs @@ -0,0 +1,9 @@ +using Content.Shared.Disposal.Components; + +namespace Content.Client.Disposal; + +[RegisterComponent] +public sealed class DisposalUnitComponent : SharedDisposalUnitComponent +{ + +} diff --git a/Content.Client/Disposal/Systems/DisposalUnitSystem.cs b/Content.Client/Disposal/Systems/DisposalUnitSystem.cs index 7f950439cc..b74e37116b 100644 --- a/Content.Client/Disposal/Systems/DisposalUnitSystem.cs +++ b/Content.Client/Disposal/Systems/DisposalUnitSystem.cs @@ -1,8 +1,12 @@ -using Content.Client.Disposal.Components; -using Content.Client.Disposal.UI; using Content.Shared.Disposal; +using Content.Shared.Disposal.Components; +using Content.Shared.DragDrop; +using Content.Shared.Emag.Systems; using Robust.Client.GameObjects; using Robust.Client.Animations; +using Robust.Client.Graphics; +using Robust.Shared.GameStates; +using Robust.Shared.Physics.Events; using static Content.Shared.Disposal.Components.SharedDisposalUnitComponent; namespace Content.Client.Disposal.Systems; @@ -12,131 +16,65 @@ public sealed class DisposalUnitSystem : SharedDisposalUnitSystem [Dependency] private readonly AppearanceSystem _appearanceSystem = default!; [Dependency] private readonly AnimationPlayerSystem _animationSystem = default!; [Dependency] private readonly SharedAudioSystem _audioSystem = default!; - private const string AnimationKey = "disposal_unit_animation"; - private readonly List _pressuringDisposals = new(); + private const string AnimationKey = "disposal_unit_animation"; public override void Initialize() { base.Initialize(); + SubscribeLocalEvent(OnHandleState); + SubscribeLocalEvent(OnPreventCollide); + SubscribeLocalEvent(OnCanDragDropOn); + SubscribeLocalEvent(OnEmagged); + SubscribeLocalEvent(OnComponentInit); SubscribeLocalEvent(OnAppearanceChange); } - public void UpdateActive(EntityUid disposalEntity, bool active) + private void OnHandleState(EntityUid uid, DisposalUnitComponent component, ref ComponentHandleState args) { - if (active) - { - if (!_pressuringDisposals.Contains(disposalEntity)) - _pressuringDisposals.Add(disposalEntity); - } - else - { - _pressuringDisposals.Remove(disposalEntity); - } - } - - public override void FrameUpdate(float frameTime) - { - base.FrameUpdate(frameTime); - for (var i = _pressuringDisposals.Count - 1; i >= 0; i--) - { - var disposal = _pressuringDisposals[i]; - if (!UpdateInterface(disposal)) - continue; - - _pressuringDisposals.RemoveAt(i); - } - } - - private bool UpdateInterface(EntityUid disposalUnit) - { - if (!TryComp(disposalUnit, out DisposalUnitComponent? component) || component.Deleted) - return true; - if (component.Deleted) - return true; - if (!TryComp(disposalUnit, out ClientUserInterfaceComponent? userInterface)) - return true; - - var state = component.UiState; - if (state == null) - return true; - - foreach (var inter in userInterface.Interfaces) - { - if (inter is DisposalUnitBoundUserInterface boundInterface) - { - return boundInterface.UpdateWindowState(state) != false; - } - } - - return true; - } - - private void OnComponentInit(EntityUid uid, DisposalUnitComponent disposalUnit, ComponentInit args) - { - if (!TryComp(uid, out var sprite)) + if (args.Current is not DisposalUnitComponentState state) return; - if (!sprite.LayerMapTryGet(DisposalUnitVisualLayers.Base, out var baseLayerIdx)) - return; // Couldn't find the "normal" layer to return to after flush animation - - if (!sprite.LayerMapTryGet(DisposalUnitVisualLayers.BaseFlush, out var flushLayerIdx)) - return; // Couldn't find the flush animation layer - - var originalBaseState = sprite.LayerGetState(baseLayerIdx); - var flushState = sprite.LayerGetState(flushLayerIdx); - - // Setup the flush animation to play - disposalUnit.FlushAnimation = new Animation - { - Length = TimeSpan.FromSeconds(disposalUnit.FlushTime), - AnimationTracks = { - new AnimationTrackSpriteFlick { - LayerKey = DisposalUnitVisualLayers.BaseFlush, - KeyFrames = { - // Play the flush animation - new AnimationTrackSpriteFlick.KeyFrame(flushState, 0), - // Return to base state (though, depending on how the unit is - // configured we might get an appearence change event telling - // us to go to charging state) - new AnimationTrackSpriteFlick.KeyFrame(originalBaseState, disposalUnit.FlushTime) - } - }, - } - }; - - if (disposalUnit.FlushSound != null) - { - disposalUnit.FlushAnimation.AnimationTracks.Add( - new AnimationTrackPlaySound - { - KeyFrames = { - new AnimationTrackPlaySound.KeyFrame(_audioSystem.GetSound(disposalUnit.FlushSound), 0) - } - }); - } - - EnsureComp(uid); - - UpdateState(uid, disposalUnit, sprite); + component.FlushSound = state.FlushSound; + component.State = state.State; + component.NextPressurized = state.NextPressurized; + component.AutomaticEngageTime = state.AutomaticEngageTime; + component.NextFlush = state.NextFlush; + component.Powered = state.Powered; + component.Engaged = state.Engaged; + component.RecentlyEjected.Clear(); + component.RecentlyEjected.AddRange(state.RecentlyEjected); } - private void OnAppearanceChange(EntityUid uid, DisposalUnitComponent unit, ref AppearanceChangeEvent args) + public override void DoInsertDisposalUnit(EntityUid uid, EntityUid toInsert, EntityUid user, SharedDisposalUnitComponent? disposal = null) + { + return; + } + + private void OnComponentInit(EntityUid uid, SharedDisposalUnitComponent sharedDisposalUnit, ComponentInit args) + { + if (!TryComp(uid, out var sprite) || !TryComp(uid, out var appearance)) + return; + + UpdateState(uid, sharedDisposalUnit, sprite, appearance); + } + + private void OnAppearanceChange(EntityUid uid, SharedDisposalUnitComponent unit, ref AppearanceChangeEvent args) { if (args.Sprite == null) - { return; - } - UpdateState(uid, unit, args.Sprite); + UpdateState(uid, unit, args.Sprite, args.Component); } - // Update visuals and tick animation - private void UpdateState(EntityUid uid, DisposalUnitComponent unit, SpriteComponent sprite) + /// + /// Update visuals and tick animation + /// + private void UpdateState(EntityUid uid, SharedDisposalUnitComponent unit, SpriteComponent sprite, AppearanceComponent appearance) { - if (!_appearanceSystem.TryGetData(uid, Visuals.VisualState, out var state)) + if (!_appearanceSystem.TryGetData(uid, Visuals.VisualState, out var state, appearance)) { return; } @@ -144,24 +82,69 @@ public sealed class DisposalUnitSystem : SharedDisposalUnitSystem sprite.LayerSetVisible(DisposalUnitVisualLayers.Unanchored, state == VisualState.UnAnchored); sprite.LayerSetVisible(DisposalUnitVisualLayers.Base, state == VisualState.Anchored); sprite.LayerSetVisible(DisposalUnitVisualLayers.BaseCharging, state == VisualState.Charging); - sprite.LayerSetVisible(DisposalUnitVisualLayers.BaseFlush, state == VisualState.Flushing); + sprite.LayerSetVisible(DisposalUnitVisualLayers.BaseFlush, state is VisualState.Flushing or VisualState.Charging); + // This is a transient state so not too worried about replaying in range. if (state == VisualState.Flushing) { if (!_animationSystem.HasRunningAnimation(uid, AnimationKey)) { - _animationSystem.Play(uid, unit.FlushAnimation, AnimationKey); + var flushState = new RSI.StateId("disposal-flush"); + + // Setup the flush animation to play + var anim = new Animation + { + Length = unit.FlushDelay, + AnimationTracks = + { + new AnimationTrackSpriteFlick + { + LayerKey = DisposalUnitVisualLayers.BaseFlush, + KeyFrames = + { + // Play the flush animation + new AnimationTrackSpriteFlick.KeyFrame(flushState, 0), + // Return to base state (though, depending on how the unit is + // configured we might get an appearance change event telling + // us to go to charging state) + new AnimationTrackSpriteFlick.KeyFrame("disposal-charging", (float) unit.FlushDelay.TotalSeconds) + } + }, + } + }; + + if (unit.FlushSound != null) + { + anim.AnimationTracks.Add( + new AnimationTrackPlaySound + { + KeyFrames = + { + new AnimationTrackPlaySound.KeyFrame(_audioSystem.GetSound(unit.FlushSound), 0) + } + }); + } + + _animationSystem.Play(uid, anim, AnimationKey); } } + else if (state == VisualState.Charging) + { + sprite.LayerSetState(DisposalUnitVisualLayers.BaseFlush, new RSI.StateId("disposal-charging")); + } + else + { + _animationSystem.Stop(uid, AnimationKey); + } - if (!_appearanceSystem.TryGetData(uid, Visuals.Handle, out var handleState)) + if (!_appearanceSystem.TryGetData(uid, Visuals.Handle, out var handleState, appearance)) { handleState = HandleState.Normal; } sprite.LayerSetVisible(DisposalUnitVisualLayers.OverlayEngaged, handleState != HandleState.Normal); - if (!_appearanceSystem.TryGetData(uid, Visuals.Light, out var lightState)) + if (!_appearanceSystem.TryGetData(uid, Visuals.Light, out var lightState, appearance)) { lightState = LightStates.Off; } diff --git a/Content.Client/Disposal/UI/DisposalUnitBoundUserInterface.cs b/Content.Client/Disposal/UI/DisposalUnitBoundUserInterface.cs index 4a56687494..a01515035e 100644 --- a/Content.Client/Disposal/UI/DisposalUnitBoundUserInterface.cs +++ b/Content.Client/Disposal/UI/DisposalUnitBoundUserInterface.cs @@ -1,6 +1,6 @@ -using Content.Client.Disposal.Components; using Content.Client.Disposal.Systems; using Content.Shared.Disposal; +using Content.Shared.Disposal.Components; using JetBrains.Annotations; using Robust.Client.GameObjects; using Robust.Client.UserInterface.Controls; @@ -16,7 +16,9 @@ namespace Content.Client.Disposal.UI { [Dependency] private readonly IEntityManager _entityManager = default!; + // What are you doing here public MailingUnitWindow? MailingUnitWindow; + public DisposalUnitWindow? DisposalUnitWindow; public DisposalUnitBoundUserInterface(ClientUserInterfaceComponent owner, Enum uiKey) : base(owner, uiKey) @@ -76,41 +78,27 @@ namespace Content.Client.Disposal.UI return; } - var entityId = Owner.Owner; - if (!_entityManager.TryGetComponent(entityId, out DisposalUnitComponent? component)) - return; - switch (state) { case MailingUnitBoundUserInterfaceState mailingUnitState: MailingUnitWindow?.UpdateState(mailingUnitState); - component.UiState = mailingUnitState.DisposalState; break; case DisposalUnitBoundUserInterfaceState disposalUnitState: DisposalUnitWindow?.UpdateState(disposalUnitState); - component.UiState = disposalUnitState; break; } - - _entityManager.System().UpdateActive(entityId, true); } protected override void Dispose(bool disposing) { base.Dispose(disposing); - if (!disposing) return; + if (!disposing) + return; MailingUnitWindow?.Dispose(); DisposalUnitWindow?.Dispose(); } - - public bool? UpdateWindowState(DisposalUnitBoundUserInterfaceState state) - { - return UiKey is DisposalUnitUiKey - ? DisposalUnitWindow?.UpdateState(state) - : MailingUnitWindow?.UpdatePressure(state.FullPressureTime); - } } } diff --git a/Content.Client/Disposal/UI/DisposalUnitWindow.xaml.cs b/Content.Client/Disposal/UI/DisposalUnitWindow.xaml.cs index 3ff40f4d48..3440fe208a 100644 --- a/Content.Client/Disposal/UI/DisposalUnitWindow.xaml.cs +++ b/Content.Client/Disposal/UI/DisposalUnitWindow.xaml.cs @@ -2,6 +2,7 @@ using Content.Shared.Disposal.Components; using Robust.Client.AutoGenerated; using Robust.Client.UserInterface.CustomControls; using Robust.Client.UserInterface.XAML; +using Robust.Shared.Timing; using static Content.Shared.Disposal.Components.SharedDisposalUnitComponent; namespace Content.Client.Disposal.UI @@ -12,6 +13,8 @@ namespace Content.Client.Disposal.UI [GenerateTypedNameReferences] public sealed partial class DisposalUnitWindow : DefaultWindow { + public TimeSpan FullPressure; + public DisposalUnitWindow() { IoCManager.InjectDependencies(this); @@ -22,14 +25,19 @@ namespace Content.Client.Disposal.UI /// Update the interface state for the disposals window. /// /// true if we should stop updating every frame. - public bool UpdateState(DisposalUnitBoundUserInterfaceState state) + public void UpdateState(DisposalUnitBoundUserInterfaceState state) { Title = state.UnitName; UnitState.Text = state.UnitState; Power.Pressed = state.Powered; Engage.Pressed = state.Engaged; + FullPressure = state.FullPressureTime; + } - return !state.Powered || PressureBar.UpdatePressure(state.FullPressureTime); + protected override void FrameUpdate(FrameEventArgs args) + { + base.FrameUpdate(args); + PressureBar.UpdatePressure(FullPressure); } } } diff --git a/Content.IntegrationTests/Tests/Disposal/DisposalUnitTest.cs b/Content.IntegrationTests/Tests/Disposal/DisposalUnitTest.cs index 3c9fdddb93..cd7da1fbd2 100644 --- a/Content.IntegrationTests/Tests/Disposal/DisposalUnitTest.cs +++ b/Content.IntegrationTests/Tests/Disposal/DisposalUnitTest.cs @@ -6,6 +6,7 @@ using Content.Server.Disposal.Unit.Components; using Content.Server.Disposal.Unit.EntitySystems; using Content.Server.Power.Components; using Content.Shared.Disposal; +using Content.Shared.Disposal.Components; using NUnit.Framework; using Robust.Shared.GameObjects; using Robust.Shared.Reflection; diff --git a/Content.Server/Disposal/Unit/Components/ActiveDisposalUnitComponent.cs b/Content.Server/Disposal/Unit/Components/ActiveDisposalUnitComponent.cs deleted file mode 100644 index cdac8ef496..0000000000 --- a/Content.Server/Disposal/Unit/Components/ActiveDisposalUnitComponent.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Content.Server.Disposal.Unit.Components; - -[RegisterComponent] -public sealed class ActiveDisposalUnitComponent : Component -{ - -} diff --git a/Content.Server/Disposal/Unit/Components/DisposalUnitComponent.cs b/Content.Server/Disposal/Unit/Components/DisposalUnitComponent.cs index 0cd7137362..67495d9b34 100644 --- a/Content.Server/Disposal/Unit/Components/DisposalUnitComponent.cs +++ b/Content.Server/Disposal/Unit/Components/DisposalUnitComponent.cs @@ -1,77 +1,13 @@ -using System.Threading; using Content.Server.Atmos; using Content.Shared.Atmos; using Content.Shared.Disposal.Components; -using Robust.Shared.Containers; -namespace Content.Server.Disposal.Unit.Components +namespace Content.Server.Disposal.Unit.Components; + +// GasMixture life. +[RegisterComponent] +public sealed class DisposalUnitComponent : SharedDisposalUnitComponent { - [RegisterComponent] - [ComponentReference(typeof(SharedDisposalUnitComponent))] - public sealed class DisposalUnitComponent : SharedDisposalUnitComponent, IGasMixtureHolder - { - /// - /// Last time that an entity tried to exit this disposal unit. - /// - [ViewVariables] - public TimeSpan LastExitAttempt; - - /// - /// The current pressure of this disposal unit. - /// Prevents it from flushing if it is not equal to or bigger than 1. - /// - [DataField("pressure")] - public float Pressure = 1f; - - [DataField("autoEngageEnabled")] - public bool AutomaticEngage = true; - - [ViewVariables(VVAccess.ReadWrite)] - [DataField("autoEngageTime")] - public readonly TimeSpan AutomaticEngageTime = TimeSpan.FromSeconds(30); - - [ViewVariables(VVAccess.ReadWrite)] - [DataField("flushDelay")] - public readonly TimeSpan FlushDelay = TimeSpan.FromSeconds(3); - - /// - /// Delay from trying to enter disposals ourselves. - /// - [ViewVariables(VVAccess.ReadWrite)] - [DataField("entryDelay")] - public float EntryDelay = 0.5f; - - /// - /// Delay from trying to shove someone else into disposals. - /// - [ViewVariables(VVAccess.ReadWrite)] - public float DraggedEntryDelay = 0.5f; - - /// - /// Token used to cancel the automatic engage of a disposal unit - /// after an entity enters it. - /// - public CancellationTokenSource? AutomaticEngageToken; - - /// - /// Container of entities inside this disposal unit. - /// - [ViewVariables] public Container Container = default!; - - [ViewVariables] public bool Powered = false; - - [ViewVariables] public PressureState State = PressureState.Ready; - - [ViewVariables(VVAccess.ReadWrite)] - public bool Engaged { get; set; } - - [DataField("air")] - public GasMixture Air { get; set; } = new(Atmospherics.CellVolume); - - [ViewVariables] - public TimeSpan NextFlush = TimeSpan.MaxValue; - - [ViewVariables] - public bool AutoFlushing = false; - } + [DataField("air")] + public GasMixture Air = new(Atmospherics.CellVolume); } diff --git a/Content.Server/Disposal/Unit/EntitySystems/DisposableSystem.cs b/Content.Server/Disposal/Unit/EntitySystems/DisposableSystem.cs index 8f42c957f1..83e079746e 100644 --- a/Content.Server/Disposal/Unit/EntitySystems/DisposableSystem.cs +++ b/Content.Server/Disposal/Unit/EntitySystems/DisposableSystem.cs @@ -4,6 +4,7 @@ using Content.Server.Disposal.Tube; using Content.Server.Disposal.Tube.Components; using Content.Server.Disposal.Unit.Components; using Content.Shared.Body.Components; +using Content.Shared.Disposal.Components; using Content.Shared.Item; using JetBrains.Annotations; using Robust.Shared.Containers; @@ -13,8 +14,7 @@ using Robust.Shared.Physics.Systems; namespace Content.Server.Disposal.Unit.EntitySystems { - [UsedImplicitly] - internal sealed class DisposableSystem : EntitySystem + public sealed class DisposableSystem : EntitySystem { [Dependency] private readonly IMapManager _mapManager = default!; [Dependency] private readonly DisposalUnitSystem _disposalUnitSystem = default!; diff --git a/Content.Server/Disposal/Unit/EntitySystems/DisposalUnitSystem.cs b/Content.Server/Disposal/Unit/EntitySystems/DisposalUnitSystem.cs index 1deb292d70..dd7742d764 100644 --- a/Content.Server/Disposal/Unit/EntitySystems/DisposalUnitSystem.cs +++ b/Content.Server/Disposal/Unit/EntitySystems/DisposalUnitSystem.cs @@ -1,5 +1,4 @@ using System.Linq; -using System.Threading; using Content.Server.Administration.Logs; using Content.Server.Atmos.EntitySystems; using Content.Server.Disposal.Tube; @@ -10,13 +9,13 @@ using Content.Server.Power.Components; using Content.Server.Power.EntitySystems; using Content.Shared.ActionBlocker; using Content.Shared.Atmos; -using Content.Shared.Construction.Components; using Content.Shared.Database; using Content.Shared.Destructible; using Content.Shared.Disposal; using Content.Shared.Disposal.Components; using Content.Shared.DoAfter; using Content.Shared.DragDrop; +using Content.Shared.Emag.Systems; using Content.Shared.Hands.Components; using Content.Shared.Hands.EntitySystems; using Content.Shared.Interaction; @@ -27,746 +26,778 @@ using Content.Shared.Throwing; using Content.Shared.Verbs; using Robust.Server.GameObjects; using Robust.Shared.Containers; +using Robust.Shared.GameStates; using Robust.Shared.Map.Components; using Robust.Shared.Physics.Components; +using Robust.Shared.Physics.Events; using Robust.Shared.Random; -using Robust.Shared.Timing; using Robust.Shared.Utility; -namespace Content.Server.Disposal.Unit.EntitySystems +namespace Content.Server.Disposal.Unit.EntitySystems; + +public sealed class DisposalUnitSystem : SharedDisposalUnitSystem { - public sealed class DisposalUnitSystem : SharedDisposalUnitSystem + [Dependency] private readonly IAdminLogManager _adminLogger = default!; + [Dependency] private readonly IRobustRandom _robustRandom = default!; + [Dependency] private readonly ActionBlockerSystem _actionBlockerSystem = default!; + [Dependency] private readonly AppearanceSystem _appearance = default!; + [Dependency] private readonly AtmosphereSystem _atmosSystem = default!; + [Dependency] private readonly DisposalTubeSystem _disposalTubeSystem = default!; + [Dependency] private readonly EntityLookupSystem _lookup = default!; + [Dependency] private readonly PopupSystem _popupSystem = default!; + [Dependency] private readonly PowerReceiverSystem _power = default!; + [Dependency] private readonly SharedContainerSystem _containerSystem = default!; + [Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!; + [Dependency] private readonly SharedHandsSystem _handsSystem = default!; + [Dependency] private readonly TransformSystem _transformSystem = default!; + [Dependency] private readonly UserInterfaceSystem _ui = default!; + + public override void Initialize() { - [Dependency] private readonly IRobustRandom _robustRandom = default!; - [Dependency] private readonly IAdminLogManager _adminLogger = default!; - [Dependency] private readonly ActionBlockerSystem _actionBlockerSystem = default!; - [Dependency] private readonly AppearanceSystem _appearance = default!; - [Dependency] private readonly AtmosphereSystem _atmosSystem = default!; - [Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!; - [Dependency] private readonly EntityLookupSystem _lookup = default!; - [Dependency] private readonly PopupSystem _popupSystem = default!; - [Dependency] private readonly SharedContainerSystem _containerSystem = default!; - [Dependency] private readonly SharedHandsSystem _handsSystem = default!; - [Dependency] private readonly TransformSystem _transformSystem = default!; - [Dependency] private readonly UserInterfaceSystem _ui = default!; - [Dependency] private readonly PowerReceiverSystem _power = default!; - [Dependency] private readonly DisposalTubeSystem _disposalTubeSystem = default!; - [Dependency] private readonly IGameTiming _gameTiming = default!; + base.Initialize(); + SubscribeLocalEvent(OnGetState); + SubscribeLocalEvent(OnPreventCollide); + SubscribeLocalEvent(OnCanDragDropOn); + SubscribeLocalEvent(OnEmagged); - public override void Initialize() + // Shouldn't need re-anchoring. + SubscribeLocalEvent(OnAnchorChanged); + SubscribeLocalEvent(OnUnpaused); + // TODO: Predict me when hands predicted + SubscribeLocalEvent(OnMovement); + SubscribeLocalEvent(OnPowerChange); + SubscribeLocalEvent(OnDisposalInit); + + SubscribeLocalEvent(OnThrowCollide); + + SubscribeLocalEvent(OnActivate); + SubscribeLocalEvent(OnAfterInteractUsing); + SubscribeLocalEvent(OnDragDropOn); + SubscribeLocalEvent(OnDestruction); + + SubscribeLocalEvent>(AddInsertVerb); + SubscribeLocalEvent>(AddDisposalAltVerbs); + SubscribeLocalEvent>(AddClimbInsideVerb); + + SubscribeLocalEvent(OnDoAfter); + + SubscribeLocalEvent(OnUiButtonPressed); + } + + private void OnGetState(EntityUid uid, DisposalUnitComponent component, ref ComponentGetState args) + { + args.State = new DisposalUnitComponentState( + component.FlushSound, + component.State, + component.NextPressurized, + component.AutomaticEngageTime, + component.NextFlush, + component.Powered, + component.Engaged, + component.RecentlyEjected); + } + + private void OnUnpaused(EntityUid uid, SharedDisposalUnitComponent component, ref EntityUnpausedEvent args) + { + if (component.NextFlush != null) + component.NextFlush = component.NextFlush.Value + args.PausedTime; + + component.NextPressurized += args.PausedTime; + } + + private void AddDisposalAltVerbs(EntityUid uid, SharedDisposalUnitComponent component, GetVerbsEvent args) + { + if (!args.CanAccess || !args.CanInteract) + return; + + // Behavior for if the disposals bin has items in it + if (component.Container.ContainedEntities.Count > 0) { - base.Initialize(); - - // Shouldn't need re-anchoring. - SubscribeLocalEvent(OnAnchorChanged); - // TODO: Predict me when hands predicted - SubscribeLocalEvent(HandleMovement); - SubscribeLocalEvent(HandlePowerChange); - - // Component lifetime - SubscribeLocalEvent(HandleDisposalInit); - SubscribeLocalEvent(HandleDisposalRemove); - - SubscribeLocalEvent(HandleThrowCollide); - - // Interactions - SubscribeLocalEvent(HandleActivate); - SubscribeLocalEvent(HandleAfterInteractUsing); - SubscribeLocalEvent(HandleDragDropOn); - SubscribeLocalEvent(HandleDestruction); - - // Verbs - SubscribeLocalEvent>(AddInsertVerb); - SubscribeLocalEvent>(AddDisposalAltVerbs); - SubscribeLocalEvent>(AddClimbInsideVerb); - - // Units - SubscribeLocalEvent(OnDoAfter); - - //UI - SubscribeLocalEvent(OnUiButtonPressed); - } - - private void AddDisposalAltVerbs(EntityUid uid, DisposalUnitComponent component, GetVerbsEvent args) - { - if (!args.CanAccess || !args.CanInteract) - return; - - // Behavior for if the disposals bin has items in it - if (component.Container.ContainedEntities.Count > 0) + // Verbs to flush the unit + AlternativeVerb flushVerb = new() { - // Verbs to flush the unit - AlternativeVerb flushVerb = new() - { - Act = () => Engage(uid, component), - Text = Loc.GetString("disposal-flush-verb-get-data-text"), - Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/delete_transparent.svg.192dpi.png")), - Priority = 1, - }; - args.Verbs.Add(flushVerb); - - // Verb to eject the contents - AlternativeVerb ejectVerb = new() - { - Act = () => TryEjectContents(uid, component), - Category = VerbCategory.Eject, - Text = Loc.GetString("disposal-eject-verb-get-data-text") - }; - args.Verbs.Add(ejectVerb); - } - } - - private void AddClimbInsideVerb(EntityUid uid, DisposalUnitComponent component, GetVerbsEvent args) - { - // This is not an interaction, activation, or alternative verb type because unfortunately most users are - // unwilling to accept that this is where they belong and don't want to accidentally climb inside. - if (!component.MobsCanEnter || - !args.CanAccess || - !args.CanInteract || - component.Container.ContainedEntities.Contains(args.User) || - !_actionBlockerSystem.CanMove(args.User)) - return; - - // Add verb to climb inside of the unit, - Verb verb = new() - { - Act = () => TryInsert(uid, args.User, args.User), - DoContactInteraction = true, - Text = Loc.GetString("disposal-self-insert-verb-get-data-text") + Act = () => ManualEngage(uid, component), + Text = Loc.GetString("disposal-flush-verb-get-data-text"), + Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/delete_transparent.svg.192dpi.png")), + Priority = 1, }; - // TODO VERB ICON - // TODO VERB CATEGORY - // create a verb category for "enter"? - // See also, medical scanner. Also maybe add verbs for entering lockers/body bags? - args.Verbs.Add(verb); - } + args.Verbs.Add(flushVerb); - private void AddInsertVerb(EntityUid uid, DisposalUnitComponent component, GetVerbsEvent args) - { - if (!args.CanAccess || !args.CanInteract || args.Hands == null || args.Using == null) - return; - - if (!_actionBlockerSystem.CanDrop(args.User)) - return; - - if (!CanInsert(uid, component, args.Using.Value)) - return; - - InteractionVerb insertVerb = new() + // Verb to eject the contents + AlternativeVerb ejectVerb = new() { - Text = Name(args.Using.Value), - Category = VerbCategory.Insert, - Act = () => - { - _handsSystem.TryDropIntoContainer(args.User, args.Using.Value, component.Container, checkActionBlocker: false, args.Hands); - _adminLogger.Add(LogType.Action, LogImpact.Medium, $"{ToPrettyString(args.User):player} inserted {ToPrettyString(args.Using.Value)} into {ToPrettyString(uid)}"); - AfterInsert(uid, component, args.Using.Value, args.User); - } + Act = () => TryEjectContents(uid, component), + Category = VerbCategory.Eject, + Text = Loc.GetString("disposal-eject-verb-get-data-text") }; + args.Verbs.Add(ejectVerb); + } + } - args.Verbs.Add(insertVerb); + private void AddClimbInsideVerb(EntityUid uid, SharedDisposalUnitComponent component, GetVerbsEvent args) + { + // This is not an interaction, activation, or alternative verb type because unfortunately most users are + // unwilling to accept that this is where they belong and don't want to accidentally climb inside. + if (!component.MobsCanEnter || + !args.CanAccess || + !args.CanInteract || + component.Container.ContainedEntities.Contains(args.User) || + !_actionBlockerSystem.CanMove(args.User)) + { + return; } - private void OnDoAfter(EntityUid uid, DisposalUnitComponent component, DoAfterEvent args) + // Add verb to climb inside of the unit, + Verb verb = new() { - if (args.Handled || args.Cancelled || args.Args.Target == null || args.Args.Used == null) - return; + Act = () => TryInsert(uid, args.User, args.User), + DoContactInteraction = true, + Text = Loc.GetString("disposal-self-insert-verb-get-data-text") + }; + // TODO VERB ICON + // TODO VERB CATEGORY + // create a verb category for "enter"? + // See also, medical scanner. Also maybe add verbs for entering lockers/body bags? + args.Verbs.Add(verb); + } - AfterInsert(uid, component, args.Args.Target.Value, args.Args.User); + private void AddInsertVerb(EntityUid uid, SharedDisposalUnitComponent component, GetVerbsEvent args) + { + if (!args.CanAccess || !args.CanInteract || args.Hands == null || args.Using == null) + return; - args.Handled = true; - } + if (!_actionBlockerSystem.CanDrop(args.User)) + return; - public void DoInsertDisposalUnit(EntityUid uid, EntityUid toInsert, EntityUid user, DisposalUnitComponent? disposal = null) + if (!CanInsert(uid, component, args.Using.Value)) + return; + + InteractionVerb insertVerb = new() { - if (!Resolve(uid, ref disposal)) - return; - - if (!disposal.Container.Insert(toInsert)) - return; - - _adminLogger.Add(LogType.Action, LogImpact.Medium, $"{ToPrettyString(user):player} inserted {ToPrettyString(toInsert)} into {ToPrettyString(uid)}"); - AfterInsert(uid, disposal, toInsert, user); - } - - public override void Update(float frameTime) - { - base.Update(frameTime); - - var query = EntityQueryEnumerator(); - while (query.MoveNext(out var uid, out var _, out var unit)) + Text = Name(args.Using.Value), + Category = VerbCategory.Insert, + Act = () => { - if (!Update(uid, unit, frameTime)) - continue; - - RemComp(uid); + _handsSystem.TryDropIntoContainer(args.User, args.Using.Value, component.Container, checkActionBlocker: false, args.Hands); + _adminLogger.Add(LogType.Action, LogImpact.Medium, $"{ToPrettyString(args.User):player} inserted {ToPrettyString(args.Using.Value)} into {ToPrettyString(uid)}"); + AfterInsert(uid, component, args.Using.Value, args.User); } + }; + + args.Verbs.Add(insertVerb); + } + + private void OnDoAfter(EntityUid uid, SharedDisposalUnitComponent component, DoAfterEvent args) + { + if (args.Handled || args.Cancelled || args.Args.Target == null || args.Args.Used == null) + return; + + AfterInsert(uid, component, args.Args.Target.Value, args.Args.User); + + args.Handled = true; + } + + public override void DoInsertDisposalUnit(EntityUid uid, EntityUid toInsert, EntityUid user, SharedDisposalUnitComponent? disposal = null) + { + if (!Resolve(uid, ref disposal)) + return; + + if (!disposal.Container.Insert(toInsert)) + return; + + _adminLogger.Add(LogType.Action, LogImpact.Medium, $"{ToPrettyString(user):player} inserted {ToPrettyString(toInsert)} into {ToPrettyString(uid)}"); + AfterInsert(uid, disposal, toInsert, user); + } + + public override void Update(float frameTime) + { + base.Update(frameTime); + + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out var unit, out var metadata)) + { + Update(uid, unit, metadata, frameTime); + } + } + + #region UI Handlers + private void OnUiButtonPressed(EntityUid uid, SharedDisposalUnitComponent component, SharedDisposalUnitComponent.UiButtonPressedMessage args) + { + if (args.Session.AttachedEntity is not { Valid: true } player) + { + return; } - #region UI Handlers - private void OnUiButtonPressed(EntityUid uid, DisposalUnitComponent component, SharedDisposalUnitComponent.UiButtonPressedMessage args) + switch (args.Button) { - if (args.Session.AttachedEntity is not { Valid: true } player) - { - return; - } + case SharedDisposalUnitComponent.UiButton.Eject: + TryEjectContents(uid, component); + _adminLogger.Add(LogType.Action, LogImpact.Low, $"{ToPrettyString(player):player} hit eject button on {ToPrettyString(uid)}"); + break; + case SharedDisposalUnitComponent.UiButton.Engage: + ToggleEngage(uid, component); + _adminLogger.Add(LogType.Action, LogImpact.Low, $"{ToPrettyString(player):player} hit flush button on {ToPrettyString(uid)}, it's now {(component.Engaged ? "on" : "off")}"); + break; + case SharedDisposalUnitComponent.UiButton.Power: + _power.TogglePower(uid, user: args.Session.AttachedEntity); + break; + default: + throw new ArgumentOutOfRangeException($"{ToPrettyString(player):player} attempted to hit a nonexistant button on {ToPrettyString(uid)}"); + } + } - switch (args.Button) - { - case SharedDisposalUnitComponent.UiButton.Eject: - TryEjectContents(uid, component); - _adminLogger.Add(LogType.Action, LogImpact.Low, $"{ToPrettyString(player):player} hit eject button on {ToPrettyString(uid)}"); - break; - case SharedDisposalUnitComponent.UiButton.Engage: - ToggleEngage(uid, component); - _adminLogger.Add(LogType.Action, LogImpact.Low, $"{ToPrettyString(player):player} hit flush button on {ToPrettyString(uid)}, it's now {(component.Engaged ? "on" : "off")}"); - break; - case SharedDisposalUnitComponent.UiButton.Power: - _power.TogglePower(uid, user: args.Session.AttachedEntity); - break; - default: - throw new ArgumentOutOfRangeException($"{ToPrettyString(player):player} attempted to hit a nonexistant button on {ToPrettyString(uid)}"); - } + public void ToggleEngage(EntityUid uid, SharedDisposalUnitComponent component) + { + component.Engaged ^= true; + + if (component.Engaged) + { + ManualEngage(uid, component); + } + else + { + Disengage(uid, component); + } + } + + #endregion + + #region Eventbus Handlers + + private void OnActivate(EntityUid uid, SharedDisposalUnitComponent component, ActivateInWorldEvent args) + { + if (!TryComp(args.User, out ActorComponent? actor)) + { + return; } - public void ToggleEngage(EntityUid uid, DisposalUnitComponent component) - { - component.Engaged ^= true; + args.Handled = true; + _ui.TryOpen(uid, SharedDisposalUnitComponent.DisposalUnitUiKey.Key, actor.PlayerSession); + } + private void OnAfterInteractUsing(EntityUid uid, SharedDisposalUnitComponent component, AfterInteractUsingEvent args) + { + if (args.Handled || !args.CanReach) + return; + + if (!HasComp(args.User)) + { + return; + } + + if (!CanInsert(uid, component, args.Used) || !_handsSystem.TryDropIntoContainer(args.User, args.Used, component.Container)) + { + return; + } + + _adminLogger.Add(LogType.Action, LogImpact.Medium, $"{ToPrettyString(args.User):player} inserted {ToPrettyString(args.Used)} into {ToPrettyString(uid)}"); + AfterInsert(uid, component, args.Used, args.User); + args.Handled = true; + } + + /// + /// Thrown items have a chance of bouncing off the unit and not going in. + /// + private void OnThrowCollide(EntityUid uid, SharedDisposalUnitComponent component, ThrowHitByEvent args) + { + if (!CanInsert(uid, component, args.Thrown) || + _robustRandom.NextDouble() > 0.75 || + !component.Container.Insert(args.Thrown)) + { + _popupSystem.PopupEntity(Loc.GetString("disposal-unit-thrown-missed"), uid); + return; + } + + if (args.User != null) + _adminLogger.Add(LogType.Landed, LogImpact.Low, $"{ToPrettyString(args.Thrown)} thrown by {ToPrettyString(args.User.Value):player} landed in {ToPrettyString(uid)}"); + + AfterInsert(uid, component, args.Thrown); + } + + private void OnDisposalInit(EntityUid uid, SharedDisposalUnitComponent component, ComponentInit args) + { + component.Container = _containerSystem.EnsureContainer(uid, SharedDisposalUnitComponent.ContainerId); + + UpdateInterface(uid, component, component.Powered); + } + + private void OnPowerChange(EntityUid uid, SharedDisposalUnitComponent component, ref PowerChangedEvent args) + { + if (!component.Running || args.Powered == component.Powered) + return; + + component.Powered = args.Powered; + UpdateVisualState(uid, component); + UpdateInterface(uid, component, args.Powered); + + if (!args.Powered) + { + component.NextFlush = null; + Dirty(component); + return; + } + + if (component.Engaged && !TryFlush(uid, component)) + { + QueueAutomaticEngage(uid, component); + } + } + + // TODO: This should just use the same thing as entity storage? + private void OnMovement(EntityUid uid, SharedDisposalUnitComponent component, ref ContainerRelayMovementEntityEvent args) + { + var currentTime = GameTiming.CurTime; + + if (!TryComp(args.Entity, out HandsComponent? hands) || + hands.Count == 0 || + currentTime < component.LastExitAttempt + ExitAttemptDelay) + { + return; + } + + component.LastExitAttempt = currentTime; + Remove(uid, component, args.Entity); + } + + private void OnAnchorChanged(EntityUid uid, SharedDisposalUnitComponent component, ref AnchorStateChangedEvent args) + { + if (Terminating(uid)) + return; + + UpdateVisualState(uid, component); + if (!args.Anchored) + TryEjectContents(uid, component); + } + + private void OnDestruction(EntityUid uid, SharedDisposalUnitComponent component, DestructionEventArgs args) + { + TryEjectContents(uid, component); + } + + private void OnDragDropOn(EntityUid uid, SharedDisposalUnitComponent component, ref DragDropTargetEvent args) + { + args.Handled = TryInsert(uid, args.Dragged, args.User); + } + + #endregion + + private void UpdateState(EntityUid uid, DisposalsPressureState state, SharedDisposalUnitComponent component, MetaDataComponent metadata) + { + if (component.State == state) + return; + + component.State = state; + UpdateVisualState(uid, component); + UpdateInterface(uid, component, component.Powered); + Dirty(component, metadata); + + if (state == DisposalsPressureState.Ready) + { + component.NextPressurized = TimeSpan.Zero; + + // Manually engaged if (component.Engaged) { - Engage(uid, component); + component.NextFlush = GameTiming.CurTime + component.ManualFlushTime; + } + else if (component.Container.ContainedEntities.Count > 0) + { + component.NextFlush = GameTiming.CurTime + component.AutomaticEngageTime; } else { - Disengage(uid, component); + component.NextFlush = null; } } - #endregion + } - #region Eventbus Handlers - private void HandleActivate(EntityUid uid, DisposalUnitComponent component, ActivateInWorldEvent args) + /// + /// Work out if we can stop updating this disposals component i.e. full pressure and nothing colliding. + /// + private void Update(EntityUid uid, SharedDisposalUnitComponent component, MetaDataComponent metadata, float frameTime) + { + var state = GetState(uid, component, metadata); + + // Pressurizing, just check if we need a state update. + if (component.NextPressurized > GameTiming.CurTime) { - if (!TryComp(args.User, out ActorComponent? actor)) - { - return; - } - - args.Handled = true; - _ui.TryOpen(uid, SharedDisposalUnitComponent.DisposalUnitUiKey.Key, actor.PlayerSession); + UpdateState(uid, state, component, metadata); + return; } - private void HandleAfterInteractUsing(EntityUid uid, DisposalUnitComponent component, AfterInteractUsingEvent args) + if (component.NextFlush != null) { - if (args.Handled || !args.CanReach) - return; - - if (!HasComp(args.User)) + if (component.NextFlush.Value < GameTiming.CurTime) { - return; - } - - if (!CanInsert(uid, component, args.Used) || !_handsSystem.TryDropIntoContainer(args.User, args.Used, component.Container)) - { - return; - } - - _adminLogger.Add(LogType.Action, LogImpact.Medium, $"{ToPrettyString(args.User):player} inserted {ToPrettyString(args.Used)} into {ToPrettyString(uid)}"); - AfterInsert(uid, component, args.Used, args.User); - args.Handled = true; - } - - /// - /// Thrown items have a chance of bouncing off the unit and not going in. - /// - private void HandleThrowCollide(EntityUid uid, DisposalUnitComponent component, ThrowHitByEvent args) - { - if (!CanInsert(uid, component, args.Thrown) || - _robustRandom.NextDouble() > 0.75 || - !component.Container.Insert(args.Thrown)) - { - _popupSystem.PopupEntity(Loc.GetString("disposal-unit-thrown-missed"), uid); - return; - } - - if (args.User != null) - _adminLogger.Add(LogType.Landed, LogImpact.Low, $"{ToPrettyString(args.Thrown)} thrown by {ToPrettyString(args.User.Value):player} landed in {ToPrettyString(uid)}"); - AfterInsert(uid, component, args.Thrown); - } - - private void HandleDisposalInit(EntityUid uid, DisposalUnitComponent component, ComponentInit args) - { - component.Container = _containerSystem.EnsureContainer(uid, SharedDisposalUnitComponent.ContainerId); - - UpdateInterface(uid, component, component.Powered); - - if (!HasComp(uid)) - { - Log.Warning($"Disposal unit {uid} is missing an {nameof(AnchorableComponent)}"); + TryFlush(uid, component); } } - private void HandleDisposalRemove(EntityUid uid, DisposalUnitComponent component, ComponentRemove args) + UpdateState(uid, state, component, metadata); + + Box2? disposalsBounds = null; + var count = component.RecentlyEjected.Count; + + if (count > 0) { - foreach (var entity in component.Container.ContainedEntities.ToArray()) + if (!HasComp(uid)) { - component.Container.Remove(entity, force: true); - } - - _ui.TryCloseAll(uid, SharedDisposalUnitComponent.DisposalUnitUiKey.Key); - component.NextFlush = TimeSpan.MaxValue; - - component.Container = null!; - RemComp(uid); - } - - private void HandlePowerChange(EntityUid uid, DisposalUnitComponent component, ref PowerChangedEvent args) - { - if (!component.Running) - return; - - component.Powered = args.Powered; - - // TODO: Need to check the other stuff. - if (!args.Powered) - { - component.NextFlush = TimeSpan.MaxValue; - } - - HandleStateChange(uid, component, args.Powered && (component.State == SharedDisposalUnitComponent.PressureState.Pressurizing || component.NextFlush != TimeSpan.MaxValue)); - UpdateVisualState(uid, component); - UpdateInterface(uid, component, args.Powered); - - if (component.Engaged && !TryFlush(uid, component)) - { - TryQueueEngage(uid, component); - } - } - - /// - /// Add or remove this disposal from the active ones for updating. - /// - public void HandleStateChange(EntityUid uid, DisposalUnitComponent _, bool active) - { - if (active) - { - EnsureComp(uid); + component.RecentlyEjected.Clear(); } else { - RemComp(uid); + disposalsBounds = _lookup.GetWorldAABB(uid); } } - private void HandleMovement(EntityUid uid, DisposalUnitComponent component, ref ContainerRelayMovementEntityEvent args) + for (var i = 0; i < component.RecentlyEjected.Count; i++) { - var currentTime = GameTiming.CurTime; - - if (!TryComp(args.Entity, out HandsComponent? hands) || - hands.Count == 0 || - currentTime < component.LastExitAttempt + ExitAttemptDelay) + var ejectedId = component.RecentlyEjected[i]; + if (HasComp(ejectedId)) { - return; - } - - component.LastExitAttempt = currentTime; - Remove(uid, component, args.Entity); - } - - private void OnAnchorChanged(EntityUid uid, DisposalUnitComponent component, ref AnchorStateChangedEvent args) - { - if (Terminating(uid)) - return; - - UpdateVisualState(uid, component); - if (!args.Anchored) - TryEjectContents(uid, component); - } - - private void HandleDestruction(EntityUid uid, DisposalUnitComponent component, DestructionEventArgs args) - { - TryEjectContents(uid, component); - } - - private void HandleDragDropOn(EntityUid uid, DisposalUnitComponent component, ref DragDropTargetEvent args) - { - args.Handled = TryInsert(uid, args.Dragged, args.User); - } - #endregion - - /// - /// Work out if we can stop updating this disposals component i.e. full pressure and nothing colliding. - /// - private bool Update(EntityUid uid, DisposalUnitComponent component, float frameTime) - { - var oldPressure = component.Pressure; - - component.Pressure = MathF.Min(1.0f, component.Pressure + PressurePerSecond * frameTime); - component.State = component.Pressure >= 1 ? SharedDisposalUnitComponent.PressureState.Ready : SharedDisposalUnitComponent.PressureState.Pressurizing; - - var state = component.State; - - if (oldPressure < 1 && state == SharedDisposalUnitComponent.PressureState.Ready) - { - UpdateVisualState(uid, component); - UpdateInterface(uid, component, component.Powered); - - if (component.Engaged) + // TODO: We need to use a specific collision method (which sloth hasn't coded yet) for actual bounds overlaps. + // TODO: Come do this sloth :^) + // Check for itemcomp as we won't just block the disposal unit "sleeping" for something it can't collide with anyway. + if (!HasComp(ejectedId) + && _lookup.GetWorldAABB(ejectedId).Intersects(disposalsBounds!.Value)) { - TryFlush(uid, component); - state = component.State; + continue; } + + component.RecentlyEjected.RemoveAt(i); + i--; } - - if (component.State == SharedDisposalUnitComponent.PressureState.Pressurizing) - { - var oldTimeElapsed = oldPressure / PressurePerSecond; - if (oldTimeElapsed < component.FlushTime && oldTimeElapsed + frameTime >= component.FlushTime) - { - // We've crossed over the amount of time it takes to flush. This will switch the - // visuals over to a 'Charging' state. - UpdateVisualState(uid, component); - } - } - else if (component.State == SharedDisposalUnitComponent.PressureState.Ready && component.NextFlush < _gameTiming.CurTime) - { - if (!TryFlush(uid, component) && component.AutoFlushing) - TryQueueEngage(uid, component); - else - component.AutoFlushing = false; - } - - Box2? disposalsBounds = null; - var count = component.RecentlyEjected.Count; - - if (count > 0) - { - if (!HasComp(uid)) - { - component.RecentlyEjected.Clear(); - } - else - { - disposalsBounds = _lookup.GetWorldAABB(uid); - } - } - - for (var i = component.RecentlyEjected.Count - 1; i >= 0; i--) - { - var ejectedId = component.RecentlyEjected[i]; - if (HasComp(ejectedId)) - { - // TODO: We need to use a specific collision method (which sloth hasn't coded yet) for actual bounds overlaps. - // TODO: Come do this sloth :^) - // Check for itemcomp as we won't just block the disposal unit "sleeping" for something it can't collide with anyway. - if (!HasComp(ejectedId) - && _lookup.GetWorldAABB(ejectedId).Intersects(disposalsBounds!.Value)) - { - continue; - } - component.RecentlyEjected.RemoveAt(i); - } - } - - if (count != component.RecentlyEjected.Count) - Dirty(component); - - return state == SharedDisposalUnitComponent.PressureState.Ready && component.NextFlush == TimeSpan.MaxValue && component.RecentlyEjected.Count == 0; } - public bool TryInsert(EntityUid unitId, EntityUid toInsertId, EntityUid? userId, DisposalUnitComponent? unit = null) + if (count != component.RecentlyEjected.Count) + Dirty(component, metadata); + } + + public bool TryInsert(EntityUid unitId, EntityUid toInsertId, EntityUid? userId, DisposalUnitComponent? unit = null) + { + if (!Resolve(unitId, ref unit)) + return false; + + if (userId.HasValue && !HasComp(userId) && toInsertId != userId) // Mobs like mouse can Jump inside even with no hands { - if (!Resolve(unitId, ref unit)) - return false; + _popupSystem.PopupEntity(Loc.GetString("disposal-unit-no-hands"), userId.Value, userId.Value, PopupType.SmallCaution); + return false; + } - if (userId.HasValue && !HasComp(userId) && toInsertId != userId) // Mobs like mouse can Jump inside even with no hands - { - _popupSystem.PopupEntity(Loc.GetString("disposal-unit-no-hands"), userId.Value, userId.Value, PopupType.SmallCaution); - return false; - } + if (!CanInsert(unitId, unit, toInsertId)) + return false; - if (!CanInsert(unitId, unit, toInsertId)) - return false; + var delay = userId == toInsertId ? unit.EntryDelay : unit.DraggedEntryDelay; - var delay = userId == toInsertId ? unit.EntryDelay : unit.DraggedEntryDelay; - - if (delay <= 0 || userId == null) - { - AfterInsert(unitId, unit, toInsertId, userId); - return true; - } - - // Can't check if our target AND disposals moves currently so we'll just check target. - // if you really want to check if disposals moves then add a predicate. - var doAfterArgs = new DoAfterArgs(userId.Value, delay, new DisposalDoAfterEvent(), unitId, target: toInsertId, used: unitId) - { - BreakOnDamage = true, - BreakOnTargetMove = true, - BreakOnUserMove = true, - NeedHand = false - }; - - _doAfterSystem.TryStartDoAfter(doAfterArgs); + if (delay <= 0 || userId == null) + { + AfterInsert(unitId, unit, toInsertId, userId); return true; } - - public bool TryFlush(EntityUid uid, DisposalUnitComponent component) + // Can't check if our target AND disposals moves currently so we'll just check target. + // if you really want to check if disposals moves then add a predicate. + var doAfterArgs = new DoAfterArgs(userId.Value, delay, new DisposalDoAfterEvent(), unitId, target: toInsertId, used: unitId) { - if (component.Deleted || !CanFlush(uid, component)) - { - return false; - } + BreakOnDamage = true, + BreakOnTargetMove = true, + BreakOnUserMove = true, + NeedHand = false + }; - component.NextFlush = TimeSpan.MaxValue; + _doAfterSystem.TryStartDoAfter(doAfterArgs); + return true; + } - //Allows the MailingUnitSystem to add tags or prevent flushing - var beforeFlushArgs = new BeforeDisposalFlushEvent(); - RaiseLocalEvent(uid, beforeFlushArgs); - if (beforeFlushArgs.Cancelled) - { - Disengage(uid, component); - return false; - } + public bool TryFlush(EntityUid uid, SharedDisposalUnitComponent component) + { + if (!CanFlush(uid, component)) + { + return false; + } - var xform = Transform(uid); - if (!TryComp(xform.GridUid, out MapGridComponent? grid)) - return false; + if (component.NextFlush != null) + component.NextFlush = component.NextFlush.Value + component.AutomaticEngageTime; - var coords = xform.Coordinates; - var entry = grid.GetLocal(coords) - .FirstOrDefault(entity => HasComp(entity)); + var beforeFlushArgs = new BeforeDisposalFlushEvent(); + RaiseLocalEvent(uid, beforeFlushArgs); - if (entry == default) - { - return false; - } + if (beforeFlushArgs.Cancelled) + { + Disengage(uid, component); + return false; + } - var air = component.Air; - var entryComponent = Comp(entry); - var indices = _transformSystem.GetGridOrMapTilePosition(uid, xform); + var xform = Transform(uid); + if (!TryComp(xform.GridUid, out MapGridComponent? grid)) + return false; - if (_atmosSystem.GetTileMixture(xform.GridUid, xform.MapUid, indices, true) is { Temperature: > 0f } environment) - { - var transferMoles = 0.1f * (0.25f * Atmospherics.OneAtmosphere * 1.01f - air.Pressure) * air.Volume / (environment.Temperature * Atmospherics.R); - - component.Air = environment.Remove(transferMoles); - } - - _disposalTubeSystem.TryInsert(entry, component, beforeFlushArgs.Tags); - - component.NextFlush = TimeSpan.MaxValue; - - if (!component.DisablePressure) - { - component.Pressure = 0; - component.State = SharedDisposalUnitComponent.PressureState.Pressurizing; - } + var coords = xform.Coordinates; + var entry = grid.GetLocal(coords) + .FirstOrDefault(HasComp); + if (entry == default || component is not DisposalUnitComponent sDisposals) + { component.Engaged = false; - - HandleStateChange(uid, component, true); - UpdateVisualState(uid, component, true); - UpdateInterface(uid, component, component.Powered); - - return true; - } - - public void UpdateInterface(EntityUid uid, DisposalUnitComponent component, bool powered) - { - var stateString = Loc.GetString($"disposal-unit-state-{component.State}"); - var state = new SharedDisposalUnitComponent.DisposalUnitBoundUserInterfaceState(Name(uid), stateString, EstimatedFullPressure(component), powered, component.Engaged); - _ui.TrySetUiState(uid, SharedDisposalUnitComponent.DisposalUnitUiKey.Key, state); - - var stateUpdatedEvent = new DisposalUnitUIStateUpdatedEvent(state); - RaiseLocalEvent(uid, stateUpdatedEvent); - } - - private TimeSpan EstimatedFullPressure(DisposalUnitComponent component) - { - if (component.State == SharedDisposalUnitComponent.PressureState.Ready) return TimeSpan.Zero; - - var currentTime = GameTiming.CurTime; - var pressure = component.Pressure; - - return TimeSpan.FromSeconds(currentTime.TotalSeconds + (1.0f - pressure) / PressurePerSecond); - } - - public void UpdateVisualState(EntityUid uid, DisposalUnitComponent component) - { - UpdateVisualState(uid, component, false); - } - - public void UpdateVisualState(EntityUid uid, DisposalUnitComponent component, bool flush) - { - if (!TryComp(uid, out AppearanceComponent? appearance)) - { - return; - } - - if (!Comp(uid).Anchored) - { - _appearance.SetData(uid, SharedDisposalUnitComponent.Visuals.VisualState, SharedDisposalUnitComponent.VisualState.UnAnchored, appearance); - _appearance.SetData(uid, SharedDisposalUnitComponent.Visuals.Handle, SharedDisposalUnitComponent.HandleState.Normal, appearance); - _appearance.SetData(uid, SharedDisposalUnitComponent.Visuals.Light, SharedDisposalUnitComponent.LightStates.Off, appearance); - return; - } - - _appearance.SetData(uid, SharedDisposalUnitComponent.Visuals.VisualState, component.Pressure < 1 ? SharedDisposalUnitComponent.VisualState.Charging : SharedDisposalUnitComponent.VisualState.Anchored, appearance); - - _appearance.SetData(uid, SharedDisposalUnitComponent.Visuals.Handle, component.Engaged - ? SharedDisposalUnitComponent.HandleState.Engaged - : SharedDisposalUnitComponent.HandleState.Normal, appearance); - - if (!component.Powered) - { - _appearance.SetData(uid, SharedDisposalUnitComponent.Visuals.Light, SharedDisposalUnitComponent.LightStates.Off, appearance); - return; - } - - var lightState = SharedDisposalUnitComponent.LightStates.Off; - if (flush) - { - _appearance.SetData(uid, SharedDisposalUnitComponent.Visuals.VisualState, SharedDisposalUnitComponent.VisualState.Flushing, appearance); - } - - if (component.Container.ContainedEntities.Count > 0) - { - lightState |= SharedDisposalUnitComponent.LightStates.Full; - } - - if (component.Pressure < 1) - { - lightState |= SharedDisposalUnitComponent.LightStates.Charging; - } - else if (!component.Engaged) - { - lightState |= SharedDisposalUnitComponent.LightStates.Ready; - } - - _appearance.SetData(uid, SharedDisposalUnitComponent.Visuals.Light, lightState, appearance); - } - - public void Remove(EntityUid uid, DisposalUnitComponent component, EntityUid toRemove) - { - component.Container.Remove(toRemove); - - if (component.Container.ContainedEntities.Count == 0) - { - component.NextFlush = TimeSpan.MaxValue; - } - - if (!component.RecentlyEjected.Contains(toRemove)) - component.RecentlyEjected.Add(toRemove); - Dirty(component); - HandleStateChange(uid, component, active: true); - UpdateVisualState(uid, component); + return false; } - public bool CanFlush(EntityUid unit, DisposalUnitComponent component) + HandleAir(uid, sDisposals, xform); + + _disposalTubeSystem.TryInsert(entry, sDisposals, beforeFlushArgs.Tags); + + component.NextPressurized = GameTiming.CurTime + TimeSpan.FromSeconds(1f / PressurePerSecond); + component.Engaged = false; + // stop queuing NOW + component.NextFlush = null; + + UpdateVisualState(uid, component, true); + UpdateInterface(uid, component, component.Powered); + + Dirty(component); + + return true; + } + + private void HandleAir(EntityUid uid, DisposalUnitComponent component, TransformComponent xform) + { + var air = component.Air; + var indices = _transformSystem.GetGridOrMapTilePosition(uid, xform); + + if (_atmosSystem.GetTileMixture(xform.GridUid, xform.MapUid, indices, true) is { Temperature: > 0f } environment) { - return component.State == SharedDisposalUnitComponent.PressureState.Ready - && component.Powered - && Comp(unit).Anchored; - } + var transferMoles = 0.1f * (0.25f * Atmospherics.OneAtmosphere * 1.01f - air.Pressure) * air.Volume / (environment.Temperature * Atmospherics.R); - public void Engage(EntityUid uid, DisposalUnitComponent component) - { - component.Engaged = true; - UpdateVisualState(uid, component); - UpdateInterface(uid, component, component.Powered); - - if (CanFlush(uid, component)) - { - component.NextFlush = _gameTiming.CurTime + component.FlushDelay; - EnsureComp(uid); - } - } - - public void Disengage(EntityUid uid, DisposalUnitComponent component) - { - component.Engaged = false; - UpdateVisualState(uid, component); - UpdateInterface(uid, component, component.Powered); - } - - /// - /// Remove all entities currently in the disposal unit. - /// - public void TryEjectContents(EntityUid uid, DisposalUnitComponent component) - { - foreach (var entity in component.Container.ContainedEntities.ToArray()) - { - Remove(uid, component, entity); - } - } - - public override bool CanInsert(EntityUid uid, SharedDisposalUnitComponent component, EntityUid entity) - { - if (!base.CanInsert(uid, component, entity) || component is not DisposalUnitComponent serverComp) - return false; - - return serverComp.Container.CanInsert(entity); - } - - /// - /// If something is inserted (or the likes) then we'll queue up a flush in the future. - /// - public void TryQueueEngage(EntityUid uid, DisposalUnitComponent component) - { - if (component.Deleted || !component.AutomaticEngage || !component.Powered && component.Container.ContainedEntities.Count == 0) - { - return; - } - - component.NextFlush = _gameTiming.CurTime + component.AutomaticEngageTime; - component.AutoFlushing = true; - - EnsureComp(uid); - } - - public void AfterInsert(EntityUid uid, DisposalUnitComponent component, EntityUid inserted, EntityUid? user = null) - { - if (!component.Container.Insert(inserted)) - return; - - if (user != inserted && user != null) - _adminLogger.Add(LogType.Action, LogImpact.Medium, $"{ToPrettyString(user.Value):player} inserted {ToPrettyString(inserted)} into {ToPrettyString(uid)}"); - - TryQueueEngage(uid, component); - - if (TryComp(inserted, out ActorComponent? actor)) - { - _ui.TryClose(uid, SharedDisposalUnitComponent.DisposalUnitUiKey.Key, actor.PlayerSession); - } - - UpdateVisualState(uid, component); + component.Air = environment.Remove(transferMoles); } } - /// - /// Sent before the disposal unit flushes it's contents. - /// Allows adding tags for sorting and preventing the disposal unit from flushing. - /// - public sealed class DisposalUnitUIStateUpdatedEvent : EntityEventArgs + public void UpdateInterface(EntityUid uid, SharedDisposalUnitComponent component, bool powered) { - public SharedDisposalUnitComponent.DisposalUnitBoundUserInterfaceState State; + var compState = GetState(uid, component); + var stateString = Loc.GetString($"disposal-unit-state-{compState}"); + var state = new SharedDisposalUnitComponent.DisposalUnitBoundUserInterfaceState(Name(uid), stateString, EstimatedFullPressure(uid, component), powered, component.Engaged); + _ui.TrySetUiState(uid, SharedDisposalUnitComponent.DisposalUnitUiKey.Key, state); - public DisposalUnitUIStateUpdatedEvent(SharedDisposalUnitComponent.DisposalUnitBoundUserInterfaceState state) - { - State = state; - } + var stateUpdatedEvent = new DisposalUnitUIStateUpdatedEvent(state); + RaiseLocalEvent(uid, stateUpdatedEvent); } /// - /// Sent before the disposal unit flushes it's contents. - /// Allows adding tags for sorting and preventing the disposal unit from flushing. + /// Returns the estimated time when the disposal unit will be back to full pressure. /// - public sealed class BeforeDisposalFlushEvent : CancellableEntityEventArgs + private TimeSpan EstimatedFullPressure(EntityUid uid, SharedDisposalUnitComponent component) { - public readonly List Tags = new(); + if (component.NextPressurized < GameTiming.CurTime) + return TimeSpan.Zero; + + return component.NextPressurized; + } + + public void UpdateVisualState(EntityUid uid, SharedDisposalUnitComponent component, bool flush = false) + { + if (!TryComp(uid, out AppearanceComponent? appearance)) + { + return; + } + + if (!Transform(uid).Anchored) + { + _appearance.SetData(uid, SharedDisposalUnitComponent.Visuals.VisualState, SharedDisposalUnitComponent.VisualState.UnAnchored, appearance); + _appearance.SetData(uid, SharedDisposalUnitComponent.Visuals.Handle, SharedDisposalUnitComponent.HandleState.Normal, appearance); + _appearance.SetData(uid, SharedDisposalUnitComponent.Visuals.Light, SharedDisposalUnitComponent.LightStates.Off, appearance); + return; + } + + var state = GetState(uid, component); + + switch (state) + { + case DisposalsPressureState.Flushed: + _appearance.SetData(uid, SharedDisposalUnitComponent.Visuals.VisualState, SharedDisposalUnitComponent.VisualState.Flushing, appearance); + break; + case DisposalsPressureState.Pressurizing: + _appearance.SetData(uid, SharedDisposalUnitComponent.Visuals.VisualState, SharedDisposalUnitComponent.VisualState.Charging, appearance); + break; + case DisposalsPressureState.Ready: + _appearance.SetData(uid, SharedDisposalUnitComponent.Visuals.VisualState, SharedDisposalUnitComponent.VisualState.Anchored, appearance); + break; + } + + _appearance.SetData(uid, SharedDisposalUnitComponent.Visuals.Handle, component.Engaged + ? SharedDisposalUnitComponent.HandleState.Engaged + : SharedDisposalUnitComponent.HandleState.Normal, appearance); + + if (!component.Powered) + { + _appearance.SetData(uid, SharedDisposalUnitComponent.Visuals.Light, SharedDisposalUnitComponent.LightStates.Off, appearance); + return; + } + + var lightState = SharedDisposalUnitComponent.LightStates.Off; + + if (component.Container.ContainedEntities.Count > 0) + { + lightState |= SharedDisposalUnitComponent.LightStates.Full; + } + + if (state is DisposalsPressureState.Pressurizing or DisposalsPressureState.Flushed) + { + lightState |= SharedDisposalUnitComponent.LightStates.Charging; + } + else + { + lightState |= SharedDisposalUnitComponent.LightStates.Ready; + } + + _appearance.SetData(uid, SharedDisposalUnitComponent.Visuals.Light, lightState, appearance); + } + + public void Remove(EntityUid uid, SharedDisposalUnitComponent component, EntityUid toRemove) + { + component.Container.Remove(toRemove); + + if (component.Container.ContainedEntities.Count == 0) + { + // If not manually engaged then reset the flushing entirely. + if (!component.Engaged) + { + component.NextFlush = null; + } + } + + if (!component.RecentlyEjected.Contains(toRemove)) + component.RecentlyEjected.Add(toRemove); + + UpdateVisualState(uid, component); + Dirty(component); + } + + public bool CanFlush(EntityUid unit, SharedDisposalUnitComponent component) + { + return GetState(unit, component) == DisposalsPressureState.Ready + && component.Powered + && Comp(unit).Anchored; + } + + public void ManualEngage(EntityUid uid, SharedDisposalUnitComponent component, MetaDataComponent? metadata = null) + { + component.Engaged = true; + UpdateVisualState(uid, component); + UpdateInterface(uid, component, component.Powered); + Dirty(component); + + if (!CanFlush(uid, component)) + return; + + if (!Resolve(uid, ref metadata)) + return; + + var pauseTime = Metadata.GetPauseTime(uid, metadata); + var nextEngage = GameTiming.CurTime - pauseTime + component.ManualFlushTime; + component.NextFlush = TimeSpan.FromSeconds(Math.Min((component.NextFlush ?? TimeSpan.MaxValue).TotalSeconds, nextEngage.TotalSeconds)); + } + + public void Disengage(EntityUid uid, SharedDisposalUnitComponent component) + { + component.Engaged = false; + + if (component.Container.ContainedEntities.Count == 0) + { + component.NextFlush = null; + } + + UpdateVisualState(uid, component); + UpdateInterface(uid, component, component.Powered); + Dirty(component); + } + + /// + /// Remove all entities currently in the disposal unit. + /// + public void TryEjectContents(EntityUid uid, SharedDisposalUnitComponent component) + { + foreach (var entity in component.Container.ContainedEntities.ToArray()) + { + Remove(uid, component, entity); + } + + if (!component.Engaged) + { + component.NextFlush = null; + Dirty(component); + } + } + + public override bool CanInsert(EntityUid uid, SharedDisposalUnitComponent component, EntityUid entity) + { + if (!base.CanInsert(uid, component, entity) || component is not SharedDisposalUnitComponent serverComp) + return false; + + return serverComp.Container.CanInsert(entity); + } + + /// + /// If something is inserted (or the likes) then we'll queue up an automatic flush in the future. + /// + public void QueueAutomaticEngage(EntityUid uid, SharedDisposalUnitComponent component, MetaDataComponent? metadata = null) + { + if (component.Deleted || !component.AutomaticEngage || !component.Powered && component.Container.ContainedEntities.Count == 0) + { + return; + } + + var pauseTime = Metadata.GetPauseTime(uid, metadata); + var automaticTime = GameTiming.CurTime + component.AutomaticEngageTime - pauseTime; + var flushTime = TimeSpan.FromSeconds(Math.Min((component.NextFlush ?? TimeSpan.MaxValue).TotalSeconds, automaticTime.TotalSeconds)); + + component.NextFlush = flushTime; + Dirty(component); + } + + public void AfterInsert(EntityUid uid, SharedDisposalUnitComponent component, EntityUid inserted, EntityUid? user = null) + { + if (!component.Container.Insert(inserted)) + return; + + if (user != inserted && user != null) + _adminLogger.Add(LogType.Action, LogImpact.Medium, $"{ToPrettyString(user.Value):player} inserted {ToPrettyString(inserted)} into {ToPrettyString(uid)}"); + + QueueAutomaticEngage(uid, component); + + if (TryComp(inserted, out ActorComponent? actor)) + { + _ui.TryClose(uid, SharedDisposalUnitComponent.DisposalUnitUiKey.Key, actor.PlayerSession); + } + + // Maybe do pullable instead? Eh still fine. + Joints.RecursiveClearJoints(inserted); + UpdateVisualState(uid, component); } } + +/// +/// Sent before the disposal unit flushes it's contents. +/// Allows adding tags for sorting and preventing the disposal unit from flushing. +/// +public sealed class DisposalUnitUIStateUpdatedEvent : EntityEventArgs +{ + public SharedDisposalUnitComponent.DisposalUnitBoundUserInterfaceState State; + + public DisposalUnitUIStateUpdatedEvent(SharedDisposalUnitComponent.DisposalUnitBoundUserInterfaceState state) + { + State = state; + } +} + +/// +/// Sent before the disposal unit flushes it's contents. +/// Allows adding tags for sorting and preventing the disposal unit from flushing. +/// +public sealed class BeforeDisposalFlushEvent : CancellableEntityEventArgs +{ + public readonly List Tags = new(); +} diff --git a/Content.Server/Storage/EntitySystems/DumpableSystem.cs b/Content.Server/Storage/EntitySystems/DumpableSystem.cs deleted file mode 100644 index dabc8dbd61..0000000000 --- a/Content.Server/Storage/EntitySystems/DumpableSystem.cs +++ /dev/null @@ -1,157 +0,0 @@ -using Content.Shared.Interaction; -using Content.Server.Storage.Components; -using Content.Shared.Storage.Components; -using Content.Shared.Verbs; -using Content.Server.Disposal.Unit.Components; -using Content.Server.Disposal.Unit.EntitySystems; -using Content.Server.Xenoarchaeology.XenoArtifacts.Triggers.Systems; -using Content.Shared.DoAfter; -using Content.Shared.Placeable; -using Content.Shared.Storage; -using Robust.Shared.Containers; -using Robust.Shared.Random; -using Robust.Shared.Utility; - -namespace Content.Server.Storage.EntitySystems -{ - public sealed class DumpableSystem : EntitySystem - { - [Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!; - [Dependency] private readonly DisposalUnitSystem _disposalUnitSystem = default!; - [Dependency] private readonly IRobustRandom _random = default!; - [Dependency] private readonly SharedContainerSystem _container = default!; - - public override void Initialize() - { - base.Initialize(); - SubscribeLocalEvent(OnAfterInteract, after: new[]{ typeof(StorageSystem) }); - SubscribeLocalEvent>(AddDumpVerb); - SubscribeLocalEvent>(AddUtilityVerbs); - SubscribeLocalEvent(OnDoAfter); - } - - private void OnAfterInteract(EntityUid uid, DumpableComponent component, AfterInteractEvent args) - { - if (!args.CanReach || args.Handled) - return; - - if (!HasComp(args.Target) && !HasComp(args.Target)) - return; - - StartDoAfter(uid, args.Target.Value, args.User, component); - args.Handled = true; - } - - private void AddDumpVerb(EntityUid uid, DumpableComponent dumpable, GetVerbsEvent args) - { - if (!args.CanAccess || !args.CanInteract) - return; - - if (!TryComp(uid, out var storage) || storage.StoredEntities == null || storage.StoredEntities.Count == 0) - return; - - AlternativeVerb verb = new() - { - Act = () => - { - StartDoAfter(uid, args.Target, args.User, dumpable);//Had multiplier of 0.6f - }, - Text = Loc.GetString("dump-verb-name"), - Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/VerbIcons/drop.svg.192dpi.png")), - }; - args.Verbs.Add(verb); - } - - private void AddUtilityVerbs(EntityUid uid, DumpableComponent dumpable, GetVerbsEvent args) - { - if (!args.CanAccess || !args.CanInteract) - return; - - if (!TryComp(uid, out var storage) || storage.StoredEntities == null || storage.StoredEntities.Count == 0) - return; - - if (HasComp(args.Target)) - { - UtilityVerb verb = new() - { - Act = () => - { - StartDoAfter(uid, args.Target, args.User, dumpable); - }, - Text = Loc.GetString("dump-disposal-verb-name", ("unit", args.Target)), - IconEntity = uid - }; - args.Verbs.Add(verb); - } - - if (HasComp(args.Target)) - { - UtilityVerb verb = new() - { - Act = () => - { - StartDoAfter(uid, args.Target, args.User, dumpable); - }, - Text = Loc.GetString("dump-placeable-verb-name", ("surface", args.Target)), - IconEntity = uid - }; - args.Verbs.Add(verb); - } - } - - public void StartDoAfter(EntityUid storageUid, EntityUid? targetUid, EntityUid userUid, DumpableComponent dumpable) - { - if (!TryComp(storageUid, out var storage) || storage.StoredEntities == null) - return; - - float delay = storage.StoredEntities.Count * (float) dumpable.DelayPerItem.TotalSeconds * dumpable.Multiplier; - - _doAfterSystem.TryStartDoAfter(new DoAfterArgs(userUid, delay, new DumpableDoAfterEvent(), storageUid, target: targetUid, used: storageUid) - { - BreakOnTargetMove = true, - BreakOnUserMove = true, - NeedHand = true - }); - } - - private void OnDoAfter(EntityUid uid, DumpableComponent component, DoAfterEvent args) - { - if (args.Handled || args.Cancelled || !TryComp(uid, out var storage) || storage.StoredEntities == null) - return; - - Queue dumpQueue = new(); - foreach (var entity in storage.StoredEntities) - { - dumpQueue.Enqueue(entity); - } - - foreach (var entity in dumpQueue) - { - var transform = Transform(entity); - _container.AttachParentToContainerOrGrid(transform); - transform.LocalPosition += _random.NextVector2Box() / 2; - transform.LocalRotation = _random.NextAngle(); - } - - if (args.Args.Target == null) - return; - - if (HasComp(args.Args.Target.Value)) - { - foreach (var entity in dumpQueue) - { - _disposalUnitSystem.DoInsertDisposalUnit(args.Args.Target.Value, entity, args.Args.User); - } - return; - } - - if (HasComp(args.Args.Target.Value)) - { - foreach (var entity in dumpQueue) - { - Transform(entity).LocalPosition = Transform(args.Args.Target.Value).LocalPosition + _random.NextVector2Box() / 4; - } - } - } - } -} diff --git a/Content.Server/Storage/EntitySystems/StorageSystem.cs b/Content.Server/Storage/EntitySystems/StorageSystem.cs index d946702e6d..25eae824c1 100644 --- a/Content.Server/Storage/EntitySystems/StorageSystem.cs +++ b/Content.Server/Storage/EntitySystems/StorageSystem.cs @@ -30,6 +30,7 @@ using Robust.Server.Player; using Robust.Shared.Audio; using Robust.Shared.Containers; using Robust.Shared.Map; +using Robust.Shared.Physics.Systems; using Robust.Shared.Player; using Robust.Shared.Random; using Robust.Shared.Timing; @@ -52,10 +53,10 @@ namespace Content.Server.Storage.EntitySystems [Dependency] private readonly SharedInteractionSystem _sharedInteractionSystem = default!; [Dependency] private readonly UserInterfaceSystem _uiSystem = default!; [Dependency] private readonly ActionBlockerSystem _actionBlockerSystem = default!; + [Dependency] private readonly SharedAppearanceSystem _appearance = default!; [Dependency] private readonly SharedAudioSystem _audio = default!; [Dependency] private readonly SharedCombatModeSystem _combatMode = default!; [Dependency] private readonly SharedTransformSystem _transform = default!; - [Dependency] private readonly SharedAppearanceSystem _appearance = default!; [Dependency] private readonly StackSystem _stack = default!; [Dependency] private readonly UseDelaySystem _useDelay = default!; @@ -226,7 +227,9 @@ namespace Content.Server.Storage.EntitySystems || !itemQuery.HasComponent(entity) || !CanInsert(uid, entity, out _, storageComp) || !_interactionSystem.InRangeUnobstructed(args.User, entity)) + { continue; + } validStorables.Add(entity); } diff --git a/Content.Shared/Disposal/Components/SharedDisposalUnitComponent.cs b/Content.Shared/Disposal/Components/SharedDisposalUnitComponent.cs index 0da7f75609..66bb5de0c6 100644 --- a/Content.Shared/Disposal/Components/SharedDisposalUnitComponent.cs +++ b/Content.Shared/Disposal/Components/SharedDisposalUnitComponent.cs @@ -1,145 +1,215 @@ +using Robust.Shared.Audio; +using Robust.Shared.Containers; using Robust.Shared.GameStates; using Robust.Shared.Serialization; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; -namespace Content.Shared.Disposal.Components +namespace Content.Shared.Disposal.Components; + +[NetworkedComponent] +public abstract class SharedDisposalUnitComponent : Component { - [NetworkedComponent] - public abstract class SharedDisposalUnitComponent : Component + public const string ContainerId = "disposals"; + + /// + /// Sounds played upon the unit flushing. + /// + [ViewVariables(VVAccess.ReadWrite), DataField("soundFlush")] + public SoundSpecifier? FlushSound = new SoundPathSpecifier("/Audio/Machines/disposalflush.ogg"); + + /// + /// State for this disposals unit. + /// + [DataField("state")] + public DisposalsPressureState State; + + // TODO: Just make this use vaulting. + /// + /// We'll track whatever just left disposals so we know what collision we need to ignore until they stop intersecting our BB. + /// + [ViewVariables, DataField("recentlyEjected")] + public readonly List RecentlyEjected = new(); + + /// + /// Next time the disposal unit will be pressurized. + /// + [DataField("nextPressurized", customTypeSerializer:typeof(TimeOffsetSerializer))] + public TimeSpan NextPressurized = TimeSpan.Zero; + + /// + /// How long it takes to flush a disposals unit manually. + /// + [DataField("flushTime")] + public TimeSpan ManualFlushTime = TimeSpan.FromSeconds(2); + + /// + /// How long it takes from the start of a flush animation to return the sprite to normal. + /// + [DataField("flushDelay")] + public TimeSpan FlushDelay = TimeSpan.FromSeconds(3); + + [DataField("mobsCanEnter")] + public bool MobsCanEnter = true; + + /// + /// Removes the pressure requirement for flushing. + /// + [DataField("disablePressure"), ViewVariables(VVAccess.ReadWrite)] + public bool DisablePressure = false; + + /// + /// Last time that an entity tried to exit this disposal unit. + /// + [ViewVariables] + public TimeSpan LastExitAttempt; + + [DataField("autoEngageEnabled")] + public bool AutomaticEngage = true; + + [ViewVariables(VVAccess.ReadWrite)] + [DataField("autoEngageTime")] + public TimeSpan AutomaticEngageTime = TimeSpan.FromSeconds(30); + + /// + /// Delay from trying to enter disposals ourselves. + /// + [ViewVariables(VVAccess.ReadWrite)] + [DataField("entryDelay")] + public float EntryDelay = 0.5f; + + /// + /// Delay from trying to shove someone else into disposals. + /// + [ViewVariables(VVAccess.ReadWrite)] + public float DraggedEntryDelay = 0.5f; + + /// + /// Container of entities inside this disposal unit. + /// + [ViewVariables] public Container Container = default!; + + // TODO: Network power shit instead fam. + [ViewVariables, DataField("powered")] + public bool Powered; + + /// + /// Was the disposals unit engaged for a manual flush. + /// + [ViewVariables(VVAccess.ReadWrite), DataField("engaged")] + public bool Engaged; + + /// + /// Next time this unit will flush. Is the lesser of and + /// + [ViewVariables, DataField("nextFlush", customTypeSerializer:typeof(TimeOffsetSerializer))] + public TimeSpan? NextFlush; + + [Serializable, NetSerializable] + public enum Visuals : byte { - public const string ContainerId = "DisposalUnit"; + VisualState, + Handle, + Light + } - // TODO: Could maybe turn the contact off instead far more cheaply as farseer (though not box2d) had support for it? - // Need to suss it out. - /// - /// We'll track whatever just left disposals so we know what collision we need to ignore until they stop intersecting our BB. - /// - public List RecentlyEjected = new(); + [Serializable, NetSerializable] + public enum VisualState : byte + { + UnAnchored, + Anchored, + Flushing, + Charging + } - [DataField("flushTime", required: true)] - public readonly float FlushTime; + [Serializable, NetSerializable] + public enum HandleState : byte + { + Normal, + Engaged + } - [DataField("mobsCanEnter")] - public bool MobsCanEnter = true; + [Serializable, NetSerializable] + [Flags] + public enum LightStates : byte + { + Off = 0, + Charging = 1 << 0, + Full = 1 << 1, + Ready = 1 << 2 + } - /// - /// Removes the pressure requirement for flushing. - /// - [DataField("disablePressure"), ViewVariables(VVAccess.ReadWrite)] - public bool DisablePressure = false; + [Serializable, NetSerializable] + public enum UiButton : byte + { + Eject, + Engage, + Power + } - [Serializable, NetSerializable] - public enum Visuals : byte + [Serializable, NetSerializable] + public sealed class DisposalUnitBoundUserInterfaceState : BoundUserInterfaceState, IEquatable + { + public readonly string UnitName; + public readonly string UnitState; + public readonly TimeSpan FullPressureTime; + public readonly bool Powered; + public readonly bool Engaged; + + public DisposalUnitBoundUserInterfaceState(string unitName, string unitState, TimeSpan fullPressureTime, bool powered, + bool engaged) { - VisualState, - Handle, - Light + UnitName = unitName; + UnitState = unitState; + FullPressureTime = fullPressureTime; + Powered = powered; + Engaged = engaged; } - [Serializable, NetSerializable] - public enum VisualState : byte + public bool Equals(DisposalUnitBoundUserInterfaceState? other) { - UnAnchored, - Anchored, - Flushing, - Charging - } - - [Serializable, NetSerializable] - public enum HandleState : byte - { - Normal, - Engaged - } - - [Serializable, NetSerializable] - public enum LightStates : byte - { - Off = 0, - Charging = 1 << 0, - Full = 1 << 1, - Ready = 1 << 2 - } - - [Serializable, NetSerializable] - public enum UiButton : byte - { - Eject, - Engage, - Power - } - - [Serializable, NetSerializable] - public enum PressureState : byte - { - Ready, - Pressurizing - } - - public override ComponentState GetComponentState() - { - return new DisposalUnitComponentState(RecentlyEjected); - } - - [Serializable, NetSerializable] - protected sealed class DisposalUnitComponentState : ComponentState - { - public List RecentlyEjected; - - public DisposalUnitComponentState(List uids) - { - RecentlyEjected = uids; - } - } - - [Serializable, NetSerializable] - public sealed class DisposalUnitBoundUserInterfaceState : BoundUserInterfaceState, IEquatable - { - public readonly string UnitName; - public readonly string UnitState; - public readonly TimeSpan FullPressureTime; - public readonly bool Powered; - public readonly bool Engaged; - - public DisposalUnitBoundUserInterfaceState(string unitName, string unitState, TimeSpan fullPressureTime, bool powered, - bool engaged) - { - UnitName = unitName; - UnitState = unitState; - FullPressureTime = fullPressureTime; - Powered = powered; - Engaged = engaged; - } - - public bool Equals(DisposalUnitBoundUserInterfaceState? other) - { - if (ReferenceEquals(null, other)) return false; - if (ReferenceEquals(this, other)) return true; - return UnitName == other.UnitName && - UnitState == other.UnitState && - Powered == other.Powered && - Engaged == other.Engaged && - FullPressureTime.Equals(other.FullPressureTime); - } - } - - /// - /// Message data sent from client to server when a disposal unit ui button is pressed. - /// - [Serializable, NetSerializable] - public sealed class UiButtonPressedMessage : BoundUserInterfaceMessage - { - public readonly UiButton Button; - - public UiButtonPressedMessage(UiButton button) - { - Button = button; - } - } - - [Serializable, NetSerializable] - public enum DisposalUnitUiKey : byte - { - Key + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return UnitName == other.UnitName && + UnitState == other.UnitState && + Powered == other.Powered && + Engaged == other.Engaged && + FullPressureTime.Equals(other.FullPressureTime); } } + + /// + /// Message data sent from client to server when a disposal unit ui button is pressed. + /// + [Serializable, NetSerializable] + public sealed class UiButtonPressedMessage : BoundUserInterfaceMessage + { + public readonly UiButton Button; + + public UiButtonPressedMessage(UiButton button) + { + Button = button; + } + } + + [Serializable, NetSerializable] + public enum DisposalUnitUiKey : byte + { + Key + } +} + +[Serializable, NetSerializable] +public enum DisposalsPressureState : byte +{ + Ready, + + /// + /// Has been flushed recently within FlushDelay. + /// + Flushed, + + /// + /// FlushDelay has elapsed and now we're transitioning back to Ready. + /// + Pressurizing } diff --git a/Content.Shared/Disposal/SharedDisposalUnitSystem.cs b/Content.Shared/Disposal/SharedDisposalUnitSystem.cs index 5e80a85d7c..7ca2959443 100644 --- a/Content.Shared/Disposal/SharedDisposalUnitSystem.cs +++ b/Content.Shared/Disposal/SharedDisposalUnitSystem.cs @@ -8,94 +8,154 @@ using Content.Shared.Mobs.Components; using Content.Shared.Mobs.Systems; using Content.Shared.Throwing; using JetBrains.Annotations; +using Robust.Shared.Audio; using Robust.Shared.Physics.Components; using Robust.Shared.Physics.Events; +using Robust.Shared.Physics.Systems; using Robust.Shared.Serialization; using Robust.Shared.Timing; -namespace Content.Shared.Disposal +namespace Content.Shared.Disposal; + +[Serializable, NetSerializable] +public sealed class DisposalDoAfterEvent : SimpleDoAfterEvent { - [Serializable, NetSerializable] - public sealed class DisposalDoAfterEvent : SimpleDoAfterEvent +} + +public abstract class SharedDisposalUnitSystem : EntitySystem +{ + [Dependency] protected readonly IGameTiming GameTiming = default!; + [Dependency] protected readonly MetaDataSystem Metadata = default!; + [Dependency] private readonly MobStateSystem _mobState = default!; + [Dependency] protected readonly SharedJointSystem Joints = default!; + + protected static TimeSpan ExitAttemptDelay = TimeSpan.FromSeconds(0.5); + + // Percentage + public const float PressurePerSecond = 0.05f; + + /// + /// Gets the current pressure state of a disposals unit. + /// + /// + /// + /// + /// + public DisposalsPressureState GetState(EntityUid uid, SharedDisposalUnitComponent component, MetaDataComponent? metadata = null) { + var nextPressure = Metadata.GetPauseTime(uid, metadata) + component.NextPressurized - GameTiming.CurTime; + var pressurizeTime = 1f / PressurePerSecond; + var pressurizeDuration = pressurizeTime - component.FlushDelay.TotalSeconds; + + if (nextPressure.TotalSeconds > pressurizeDuration) + { + return DisposalsPressureState.Flushed; + } + + if (nextPressure > TimeSpan.Zero) + { + return DisposalsPressureState.Pressurizing; + } + + return DisposalsPressureState.Ready; } - [UsedImplicitly] - public abstract class SharedDisposalUnitSystem : EntitySystem + public float GetPressure(EntityUid uid, SharedDisposalUnitComponent component, MetaDataComponent? metadata = null) { - [Dependency] protected readonly IGameTiming GameTiming = default!; - [Dependency] private readonly MobStateSystem _mobState = default!; + if (!Resolve(uid, ref metadata)) + return 0f; - protected static TimeSpan ExitAttemptDelay = TimeSpan.FromSeconds(0.5); + var pauseTime = Metadata.GetPauseTime(uid, metadata); + return MathF.Min(1f, + (float) (GameTiming.CurTime - pauseTime - component.NextPressurized).TotalSeconds / PressurePerSecond); + } - // Percentage - public const float PressurePerSecond = 0.05f; + protected void OnPreventCollide(EntityUid uid, SharedDisposalUnitComponent component, + ref PreventCollideEvent args) + { + var otherBody = args.OtherEntity; - public override void Initialize() + // Items dropped shouldn't collide but items thrown should + if (EntityManager.HasComponent(otherBody) && + !EntityManager.HasComponent(otherBody)) { - base.Initialize(); - SubscribeLocalEvent(OnPreventCollide); - SubscribeLocalEvent(OnCanDragDropOn); - SubscribeLocalEvent(OnEmagged); + args.Cancelled = true; + return; } - private void OnPreventCollide(EntityUid uid, SharedDisposalUnitComponent component, - ref PreventCollideEvent args) + if (component.RecentlyEjected.Contains(otherBody)) { - var otherBody = args.OtherEntity; + args.Cancelled = true; + } + } - // Items dropped shouldn't collide but items thrown should - if (EntityManager.HasComponent(otherBody) && - !EntityManager.HasComponent(otherBody)) - { - args.Cancelled = true; - return; - } + protected void OnCanDragDropOn(EntityUid uid, SharedDisposalUnitComponent component, ref CanDropTargetEvent args) + { + if (args.Handled) + return; - if (component.RecentlyEjected.Contains(otherBody)) - { - args.Cancelled = true; - } + args.CanDrop = CanInsert(uid, component, args.Dragged); + args.Handled = true; + } + + protected void OnEmagged(EntityUid uid, SharedDisposalUnitComponent component, ref GotEmaggedEvent args) + { + component.DisablePressure = true; + args.Handled = true; + } + + public virtual bool CanInsert(EntityUid uid, SharedDisposalUnitComponent component, EntityUid entity) + { + if (!EntityManager.GetComponent(uid).Anchored) + return false; + + // TODO: Probably just need a disposable tag. + if (!EntityManager.TryGetComponent(entity, out ItemComponent? storable) && + !EntityManager.HasComponent(entity)) + { + return false; } - private void OnCanDragDropOn(EntityUid uid, SharedDisposalUnitComponent component, ref CanDropTargetEvent args) - { - if (args.Handled) - return; + //Check if the entity is a mob and if mobs can be inserted + if (TryComp(entity, out var damageState) && !component.MobsCanEnter) + return false; - args.CanDrop = CanInsert(uid, component, args.Dragged); - args.Handled = true; + if (EntityManager.TryGetComponent(entity, out PhysicsComponent? physics) && + (physics.CanCollide || storable != null)) + { + return true; } - private void OnEmagged(EntityUid uid, SharedDisposalUnitComponent component, ref GotEmaggedEvent args) + return damageState != null && (!component.MobsCanEnter || _mobState.IsDead(entity, damageState)); + } + + /// + /// TODO: Proper prediction + /// + public abstract void DoInsertDisposalUnit(EntityUid uid, EntityUid toInsert, EntityUid user, SharedDisposalUnitComponent? disposal = null); + + [Serializable, NetSerializable] + protected sealed class DisposalUnitComponentState : ComponentState + { + public SoundSpecifier? FlushSound; + public DisposalsPressureState State; + public TimeSpan NextPressurized; + public TimeSpan AutomaticEngageTime; + public TimeSpan? NextFlush; + public bool Powered; + public bool Engaged; + public List RecentlyEjected; + + public DisposalUnitComponentState(SoundSpecifier? flushSound, DisposalsPressureState state, TimeSpan nextPressurized, TimeSpan automaticEngageTime, TimeSpan? nextFlush, bool powered, bool engaged, List recentlyEjected) { - component.DisablePressure = true; - args.Handled = true; - } - - public virtual bool CanInsert(EntityUid uid, SharedDisposalUnitComponent component, EntityUid entity) - { - if (!EntityManager.GetComponent(uid).Anchored) - return false; - - // TODO: Probably just need a disposable tag. - if (!EntityManager.TryGetComponent(entity, out ItemComponent? storable) && - !EntityManager.HasComponent(entity)) - { - return false; - } - - //Check if the entity is a mob and if mobs can be inserted - if (TryComp(entity, out var damageState) && !component.MobsCanEnter) - return false; - - if (EntityManager.TryGetComponent(entity, out PhysicsComponent? physics) && - (physics.CanCollide || storable != null)) - { - return true; - } - - return damageState != null && (!component.MobsCanEnter || _mobState.IsDead(entity, damageState)); + FlushSound = flushSound; + State = state; + NextPressurized = nextPressurized; + AutomaticEngageTime = automaticEngageTime; + NextFlush = nextFlush; + Powered = powered; + Engaged = engaged; + RecentlyEjected = recentlyEjected; } } } diff --git a/Content.Shared/Storage/Components/DumpableComponent.cs b/Content.Shared/Storage/Components/DumpableComponent.cs index ac7003d675..8ef14e1bb6 100644 --- a/Content.Shared/Storage/Components/DumpableComponent.cs +++ b/Content.Shared/Storage/Components/DumpableComponent.cs @@ -1,29 +1,30 @@ -using System.Threading; using Content.Shared.DoAfter; +using Robust.Shared.GameStates; using Robust.Shared.Serialization; -namespace Content.Shared.Storage.Components +namespace Content.Shared.Storage.Components; + +[Serializable, NetSerializable] +public sealed class DumpableDoAfterEvent : SimpleDoAfterEvent { - [Serializable, NetSerializable] - public sealed class DumpableDoAfterEvent : SimpleDoAfterEvent - { - } +} + +/// +/// Lets you dump this container on the ground using a verb, +/// or when interacting with it on a disposal unit or placeable surface. +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +public sealed partial class DumpableComponent : Component +{ + /// + /// How long each item adds to the doafter. + /// + [DataField("delayPerItem"), AutoNetworkedField] + public TimeSpan DelayPerItem = TimeSpan.FromSeconds(0.2); /// - /// Lets you dump this container on the ground using a verb, - /// or when interacting with it on a disposal unit or placeable surface. + /// The multiplier modifier /// - [RegisterComponent] - public sealed class DumpableComponent : Component - { - /// - /// How long each item adds to the doafter. - /// - [DataField("delayPerItem")] public TimeSpan DelayPerItem = TimeSpan.FromSeconds(0.2); - - /// - /// The multiplier modifier - /// - [DataField("multiplier")] public float Multiplier = 1.0f; - } + [DataField("multiplier"), AutoNetworkedField] + public float Multiplier = 1.0f; } diff --git a/Content.Shared/Storage/EntitySystems/DumpableSystem.cs b/Content.Shared/Storage/EntitySystems/DumpableSystem.cs new file mode 100644 index 0000000000..8cb68d97c4 --- /dev/null +++ b/Content.Shared/Storage/EntitySystems/DumpableSystem.cs @@ -0,0 +1,158 @@ +using Content.Shared.Disposal; +using Content.Shared.Disposal.Components; +using Content.Shared.DoAfter; +using Content.Shared.Interaction; +using Content.Shared.Placeable; +using Content.Shared.Storage.Components; +using Content.Shared.Verbs; +using Robust.Shared.Containers; +using Robust.Shared.Random; +using Robust.Shared.Utility; + +namespace Content.Shared.Storage.EntitySystems; + +public sealed class DumpableSystem : EntitySystem +{ + [Dependency] private readonly IRobustRandom _random = default!; + [Dependency] private readonly SharedContainerSystem _container = default!; + [Dependency] private readonly SharedDisposalUnitSystem _disposalUnitSystem = default!; + [Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!; + [Dependency] private readonly SharedTransformSystem _transformSystem = default!; + + private EntityQuery _xformQuery; + + public override void Initialize() + { + base.Initialize(); + _xformQuery = GetEntityQuery(); + SubscribeLocalEvent(OnAfterInteract, after: new[]{ typeof(SharedEntityStorageSystem) }); + SubscribeLocalEvent>(AddDumpVerb); + SubscribeLocalEvent>(AddUtilityVerbs); + SubscribeLocalEvent(OnDoAfter); + } + + private void OnAfterInteract(EntityUid uid, DumpableComponent component, AfterInteractEvent args) + { + if (!args.CanReach || args.Handled) + return; + + if (!HasComp(args.Target) && !HasComp(args.Target)) + return; + + StartDoAfter(uid, args.Target.Value, args.User, component); + args.Handled = true; + } + + private void AddDumpVerb(EntityUid uid, DumpableComponent dumpable, GetVerbsEvent args) + { + if (!args.CanAccess || !args.CanInteract) + return; + + if (!TryComp(uid, out var storage) || storage.StoredEntities == null || storage.StoredEntities.Count == 0) + return; + + AlternativeVerb verb = new() + { + Act = () => + { + StartDoAfter(uid, args.Target, args.User, dumpable);//Had multiplier of 0.6f + }, + Text = Loc.GetString("dump-verb-name"), + Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/VerbIcons/drop.svg.192dpi.png")), + }; + args.Verbs.Add(verb); + } + + private void AddUtilityVerbs(EntityUid uid, DumpableComponent dumpable, GetVerbsEvent args) + { + if (!args.CanAccess || !args.CanInteract) + return; + + if (!TryComp(uid, out var storage) || storage.StoredEntities == null || storage.StoredEntities.Count == 0) + return; + + if (HasComp(args.Target)) + { + UtilityVerb verb = new() + { + Act = () => + { + StartDoAfter(uid, args.Target, args.User, dumpable); + }, + Text = Loc.GetString("dump-disposal-verb-name", ("unit", args.Target)), + IconEntity = uid + }; + args.Verbs.Add(verb); + } + + if (HasComp(args.Target)) + { + UtilityVerb verb = new() + { + Act = () => + { + StartDoAfter(uid, args.Target, args.User, dumpable); + }, + Text = Loc.GetString("dump-placeable-verb-name", ("surface", args.Target)), + IconEntity = uid + }; + args.Verbs.Add(verb); + } + } + + public void StartDoAfter(EntityUid storageUid, EntityUid? targetUid, EntityUid userUid, DumpableComponent dumpable) + { + if (!TryComp(storageUid, out var storage) || storage.StoredEntities == null) + return; + + float delay = storage.StoredEntities.Count * (float) dumpable.DelayPerItem.TotalSeconds * dumpable.Multiplier; + + _doAfterSystem.TryStartDoAfter(new DoAfterArgs(userUid, delay, new DumpableDoAfterEvent(), storageUid, target: targetUid, used: storageUid) + { + BreakOnTargetMove = true, + BreakOnUserMove = true, + NeedHand = true + }); + } + + private void OnDoAfter(EntityUid uid, DumpableComponent component, DoAfterEvent args) + { + if (args.Handled || args.Cancelled || !TryComp(uid, out var storage) || storage.StoredEntities == null) + return; + + Queue dumpQueue = new(); + foreach (var entity in storage.StoredEntities) + { + dumpQueue.Enqueue(entity); + } + + foreach (var entity in dumpQueue) + { + var transform = Transform(entity); + _container.AttachParentToContainerOrGrid(transform); + _transformSystem.SetLocalPositionRotation(transform, transform.LocalPosition + _random.NextVector2Box() / 2, _random.NextAngle()); + } + + if (args.Args.Target == null) + return; + + if (HasComp(args.Args.Target.Value)) + { + foreach (var entity in dumpQueue) + { + _disposalUnitSystem.DoInsertDisposalUnit(args.Args.Target.Value, entity, args.Args.User); + } + return; + } + + if (HasComp(args.Args.Target.Value)) + { + var targetPos = _xformQuery.GetComponent(args.Args.Target.Value).LocalPosition; + + foreach (var entity in dumpQueue) + { + _transformSystem.SetLocalPosition(entity, targetPos + _random.NextVector2Box() / 4); + } + } + } +} diff --git a/Content.Shared/Storage/EntitySystems/SharedEntityStorageSystem.cs b/Content.Shared/Storage/EntitySystems/SharedEntityStorageSystem.cs index da27be0c93..da8e6cc18e 100644 --- a/Content.Shared/Storage/EntitySystems/SharedEntityStorageSystem.cs +++ b/Content.Shared/Storage/EntitySystems/SharedEntityStorageSystem.cs @@ -34,6 +34,7 @@ public abstract class SharedEntityStorageSystem : EntitySystem [Dependency] private readonly SharedAudioSystem _audio = default!; [Dependency] private readonly SharedContainerSystem _container = default!; [Dependency] private readonly SharedInteractionSystem _interaction = default!; + [Dependency] private readonly SharedJointSystem _joints = default!; [Dependency] private readonly SharedPhysicsSystem _physics = default!; [Dependency] protected readonly SharedPopupSystem Popup = default!; [Dependency] private readonly SharedTransformSystem _transform = default!; @@ -261,6 +262,7 @@ public abstract class SharedEntityStorageSystem : EntitySystem return true; } + _joints.RecursiveClearJoints(toInsert); var inside = EnsureComp(toInsert); inside.Storage = container; return component.Contents.Insert(toInsert, EntityManager); diff --git a/Content.Shared/Weapons/Misc/SharedGrapplingGunSystem.cs b/Content.Shared/Weapons/Misc/SharedGrapplingGunSystem.cs index 0787c4a28d..aa2cf4feb9 100644 --- a/Content.Shared/Weapons/Misc/SharedGrapplingGunSystem.cs +++ b/Content.Shared/Weapons/Misc/SharedGrapplingGunSystem.cs @@ -37,6 +37,7 @@ public abstract class SharedGrapplingGunSystem : EntitySystem { base.Initialize(); SubscribeLocalEvent(OnGrappleCollide); + SubscribeLocalEvent(OnGrappleJointRemoved); SubscribeLocalEvent(OnWeightlessMove); SubscribeAllEvent(OnGrapplingReel); @@ -45,6 +46,11 @@ public abstract class SharedGrapplingGunSystem : EntitySystem SubscribeLocalEvent(OnGrapplingDeselected); } + private void OnGrappleJointRemoved(EntityUid uid, GrapplingProjectileComponent component, JointRemovedEvent args) + { + QueueDel(uid); + } + private void OnGrapplingShot(EntityUid uid, GrapplingGunComponent component, ref GunShotEvent args) { foreach (var (shotUid, _) in args.Ammo) diff --git a/Resources/Locale/en-US/disposal/unit/components/disposal-unit-component.ftl b/Resources/Locale/en-US/disposal/unit/components/disposal-unit-component.ftl index f5989003ab..60854b8254 100644 --- a/Resources/Locale/en-US/disposal/unit/components/disposal-unit-component.ftl +++ b/Resources/Locale/en-US/disposal/unit/components/disposal-unit-component.ftl @@ -23,4 +23,6 @@ disposal-unit-thrown-missed = Missed! # state disposal-unit-state-Ready = Ready +# Yes I want it to always say Pressurizing +disposal-unit-state-Flushed = Pressurizing disposal-unit-state-Pressurizing = Pressurizing diff --git a/Resources/Maps/Salvage/small-4.yml b/Resources/Maps/Salvage/small-4.yml index ea610e0e4a..cca99efb9c 100644 --- a/Resources/Maps/Salvage/small-4.yml +++ b/Resources/Maps/Salvage/small-4.yml @@ -178,7 +178,7 @@ entities: parent: 37 type: Transform - containers: - DisposalUnit: !type:Container + disposals: !type:Container showEnts: False occludes: True ents: diff --git a/Resources/Prototypes/Entities/Structures/Piping/Disposal/units.yml b/Resources/Prototypes/Entities/Structures/Piping/Disposal/units.yml index d6268b4006..c8792bc3ba 100644 --- a/Resources/Prototypes/Entities/Structures/Piping/Disposal/units.yml +++ b/Resources/Prototypes/Entities/Structures/Piping/Disposal/units.yml @@ -16,8 +16,6 @@ map: [ "enum.DisposalUnitVisualLayers.Unanchored" ] - state: disposal map: [ "enum.DisposalUnitVisualLayers.Base" ] - - state: disposal-charging - map: [ "enum.DisposalUnitVisualLayers.BaseCharging" ] - state: disposal-flush map: [ "enum.DisposalUnitVisualLayers.BaseFlush" ] - state: dispover-charge @@ -70,7 +68,7 @@ type: DisposalUnitBoundUserInterface - type: ContainerContainer containers: - DisposalUnit: !type:Container + disposals: !type:Container - type: StaticPrice price: 62 - type: PowerSwitch @@ -84,9 +82,6 @@ graph: DisposalMachine node: disposal_unit - type: DisposalUnit - flushSound: - path: /Audio/Machines/disposalflush.ogg - flushTime: 2 - type: UserInterface interfaces: - key: enum.DisposalUnitUiKey.Key @@ -123,9 +118,6 @@ - type: DisposalUnit autoEngageEnabled: false mobsCanEnter: false - flushSound: - path: /Audio/Machines/disposalflush.ogg - flushTime: 2 - type: MailingUnit - type: DeviceNetwork deviceNetId: Wired