diff --git a/Content.Client/Storage/ClientStorageComponent.cs b/Content.Client/Storage/ClientStorageComponent.cs index f1042eb90c..122d2e9900 100644 --- a/Content.Client/Storage/ClientStorageComponent.cs +++ b/Content.Client/Storage/ClientStorageComponent.cs @@ -8,7 +8,7 @@ namespace Content.Client.Storage /// Client version of item storage containers, contains a UI which displays stored entities and their size /// [RegisterComponent] - public sealed class ClientStorageComponent : SharedStorageComponent, IDraggable + public sealed class ClientStorageComponent : SharedStorageComponent { [Dependency] private readonly IEntityManager _entityManager = default!; private List _storedEntities = new(); diff --git a/Content.Server/Disposal/Unit/EntitySystems/DisposalUnitSystem.cs b/Content.Server/Disposal/Unit/EntitySystems/DisposalUnitSystem.cs index c40b7adc28..9857624481 100644 --- a/Content.Server/Disposal/Unit/EntitySystems/DisposalUnitSystem.cs +++ b/Content.Server/Disposal/Unit/EntitySystems/DisposalUnitSystem.cs @@ -162,6 +162,17 @@ namespace Content.Server.Disposal.Unit.EntitySystems AfterInsert(unit, toInsert); } + public void DoInsertDisposalUnit(EntityUid unit, EntityUid toInsert, DisposalUnitComponent? disposal = null) + { + if (!Resolve(unit, ref disposal)) + return; + + if (!disposal.Container.Insert(toInsert)) + return; + + AfterInsert(disposal, toInsert); + } + public override void Update(float frameTime) { base.Update(frameTime); diff --git a/Content.Server/Storage/EntitySystems/DumpableSystem.cs b/Content.Server/Storage/EntitySystems/DumpableSystem.cs new file mode 100644 index 0000000000..8890b18d18 --- /dev/null +++ b/Content.Server/Storage/EntitySystems/DumpableSystem.cs @@ -0,0 +1,193 @@ +using System.Threading; +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.DoAfter; +using Content.Shared.Placeable; +using Content.Server.Hands.Systems; +using Robust.Shared.Containers; + +namespace Content.Server.Storage.EntitySystems +{ + public sealed class DumpableSystem : EntitySystem + { + [Dependency] private readonly DoAfterSystem _doAfterSystem = default!; + [Dependency] private readonly DisposalUnitSystem _disposalUnitSystem = default!; + + [Dependency] private readonly HandsSystem _handsSystem = default!; + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent(OnAfterInteract); + SubscribeLocalEvent>(AddDumpVerb); + SubscribeLocalEvent>(AddUtilityVerbs); + SubscribeLocalEvent(OnDumpCompleted); + SubscribeLocalEvent(OnDumpCancelled); + } + + private void OnAfterInteract(EntityUid uid, DumpableComponent component, AfterInteractEvent args) + { + if (!args.CanReach) + return; + + if (!TryComp(args.Used, out var storage)) + return; + + if (storage.StoredEntities == null || storage.StoredEntities.Count == 0) + return; + + if (HasComp(args.Target) || HasComp(args.Target)) + { + StartDoAfter(uid, args.Target.Value, args.User, component, storage); + return; + } + } + 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, null, args.User, dumpable, storage, 0.6f); + }, + Text = Loc.GetString("dump-verb-name"), + IconTexture = "/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, storage); + }, + 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, storage); + }, + Text = Loc.GetString("dump-placeable-verb-name", ("surface", args.Target)), + IconEntity = uid + }; + args.Verbs.Add(verb); + } + } + + private void StartDoAfter(EntityUid storageUid, EntityUid? targetUid, EntityUid userUid, DumpableComponent dumpable, ServerStorageComponent storage, float multiplier = 1) + { + if (dumpable.CancelToken != null) + { + dumpable.CancelToken.Cancel(); + dumpable.CancelToken = null; + return; + } + + if (storage.StoredEntities == null) + return; + + float delay = storage.StoredEntities.Count * (float) dumpable.DelayPerItem.TotalSeconds * multiplier; + + dumpable.CancelToken = new CancellationTokenSource(); + _doAfterSystem.DoAfter(new DoAfterEventArgs(userUid, delay, dumpable.CancelToken.Token, target: targetUid) + { + BroadcastFinishedEvent = new DumpCompletedEvent(userUid, targetUid, storage.StoredEntities), + BroadcastCancelledEvent = new DumpCancelledEvent(dumpable.Owner), + BreakOnTargetMove = true, + BreakOnUserMove = true, + BreakOnStun = true, + NeedHand = true + }); + } + + + private void OnDumpCompleted(DumpCompletedEvent args) + { + Queue dumpQueue = new(); + foreach (var entity in args.StoredEntities) + { + dumpQueue.Enqueue(entity); + } + + if (TryComp(args.Target, out var disposal)) + { + foreach (var entity in dumpQueue) + { + _disposalUnitSystem.DoInsertDisposalUnit(args.Target.Value, entity); + } + return; + } + + foreach (var entity in dumpQueue) + { + Transform(entity).AttachParentToContainerOrGrid(EntityManager); + } + + if (HasComp(args.Target)) + { + foreach (var entity in dumpQueue) + { + Transform(entity).LocalPosition = Transform(args.Target.Value).LocalPosition; + } + return; + } + } + + private void OnDumpCancelled(DumpCancelledEvent args) + { + if (TryComp(args.Uid, out var dumpable)) + dumpable.CancelToken = null; + } + + private sealed class DumpCancelledEvent : EntityEventArgs + { + public readonly EntityUid Uid; + public DumpCancelledEvent(EntityUid uid) + { + Uid = uid; + } + } + + private sealed class DumpCompletedEvent : EntityEventArgs + { + public EntityUid User { get; } + public EntityUid? Target { get; } + public IReadOnlyList StoredEntities { get; } + + public DumpCompletedEvent(EntityUid user, EntityUid? target, IReadOnlyList storedEntities) + { + User = user; + Target = target; + StoredEntities = storedEntities; + } + } + } +} diff --git a/Content.Server/Storage/EntitySystems/StorageSystem.cs b/Content.Server/Storage/EntitySystems/StorageSystem.cs index af87eb8c1b..847695f0ed 100644 --- a/Content.Server/Storage/EntitySystems/StorageSystem.cs +++ b/Content.Server/Storage/EntitySystems/StorageSystem.cs @@ -175,19 +175,6 @@ namespace Content.Server.Storage.EntitySystems args.Verbs.Add(verb); } - - // if the target is a disposal unit, add a verb to transfer storage into the unit (e.g., empty a trash bag). - if (!TryComp(args.Target, out DisposalUnitComponent? disposal)) - return; - - UtilityVerb dispose = new() - { - Text = Loc.GetString("storage-component-dispose-verb"), - IconEntity = args.Using, - Act = () => DisposeEntities(args.User, uid, args.Target, component, lockComponent, disposal) - }; - - args.Verbs.Add(dispose); } @@ -446,35 +433,6 @@ namespace Content.Server.Storage.EntitySystems UpdateStorageUI(source, sourceComp); } - /// - /// Move entities from storage into a disposal unit. - /// - public void DisposeEntities(EntityUid user, EntityUid source, EntityUid target, - ServerStorageComponent? sourceComp = null, LockComponent? sourceLock = null, - DisposalUnitComponent? disposalComp = null) - { - if (!Resolve(source, ref sourceComp) || !Resolve(target, ref disposalComp)) - return; - - var entities = sourceComp.Storage?.ContainedEntities; - if (entities == null || entities.Count == 0) - return; - - if (Resolve(source, ref sourceLock, false) && sourceLock.Locked) - return; - - foreach (var entity in entities.ToList()) - { - if (_disposalSystem.CanInsert(disposalComp, entity) - && disposalComp.Container.Insert(entity)) - { - _disposalSystem.AfterInsert(disposalComp, entity); - } - } - RecalculateStorageUsed(sourceComp); - UpdateStorageUI(source, sourceComp); - } - public void HandleRemoveEntity(EntityUid uid, EntityUid player, EntityUid itemToRemove, ServerStorageComponent? storageComp = null) { if (!Resolve(uid, ref storageComp)) diff --git a/Content.Shared/Placeable/PlaceableSurfaceSystem.cs b/Content.Shared/Placeable/PlaceableSurfaceSystem.cs index a7153d098e..949b28b93b 100644 --- a/Content.Shared/Placeable/PlaceableSurfaceSystem.cs +++ b/Content.Shared/Placeable/PlaceableSurfaceSystem.cs @@ -1,4 +1,4 @@ -using Content.Shared.Hands.Components; +using Content.Shared.Storage.Components; using Content.Shared.Hands.EntitySystems; using Content.Shared.Interaction; using Robust.Shared.GameStates; @@ -52,6 +52,11 @@ namespace Content.Shared.Placeable if (!surface.IsPlaceable) return; + // 99% of the time they want to dump the stuff inside on the table, they can manually place with q if they really need to. + // Just causes prediction CBT otherwise. + if (HasComp(args.Used)) + return; + if (!_handsSystem.TryDrop(args.User, args.Used)) return; diff --git a/Content.Shared/Storage/Components/DumpableComponent.cs b/Content.Shared/Storage/Components/DumpableComponent.cs new file mode 100644 index 0000000000..4ab1e70651 --- /dev/null +++ b/Content.Shared/Storage/Components/DumpableComponent.cs @@ -0,0 +1,23 @@ +using System.Threading; + +namespace Content.Shared.Storage.Components +{ + /// + /// Lets you dump this container on the ground using a verb, + /// or when interacting with it on a disposal unit or placeable surface. + /// + [RegisterComponent] + public sealed class DumpableComponent : Component + { + /// + /// How long each item adds to the doafter. + /// + [DataField("delayPerItem")] + public TimeSpan DelayPerItem = TimeSpan.FromSeconds(0.2); + + /// + /// Cancellation token for the doafter. + /// + public CancellationTokenSource? CancelToken; + } +} diff --git a/Content.Shared/Storage/SharedStorageComponent.cs b/Content.Shared/Storage/SharedStorageComponent.cs index 8391af9123..cf6738ba43 100644 --- a/Content.Shared/Storage/SharedStorageComponent.cs +++ b/Content.Shared/Storage/SharedStorageComponent.cs @@ -9,7 +9,7 @@ using Robust.Shared.Serialization; namespace Content.Shared.Storage { [NetworkedComponent()] - public abstract class SharedStorageComponent : Component, IDraggable + public abstract class SharedStorageComponent : Component { [Serializable, NetSerializable] public sealed class StorageBoundUserInterfaceState : BoundUserInterfaceState @@ -56,32 +56,6 @@ namespace Content.Shared.Storage /// The entity to remove /// True if no longer in storage, false otherwise public abstract bool Remove(EntityUid entity); - - bool IDraggable.CanDrop(CanDropEvent args) - { - return _entMan.TryGetComponent(args.Target, out PlaceableSurfaceComponent? placeable) && - placeable.IsPlaceable; - } - - bool IDraggable.Drop(DragDropEvent eventArgs) - { - if (!EntitySystem.Get().CanInteract(eventArgs.User, eventArgs.Target)) - return false; - - var storedEntities = StoredEntities?.ToArray(); - - if (storedEntities == null) - return false; - - // empty everything out - foreach (var storedEntity in storedEntities) - { - if (Remove(storedEntity)) - _entMan.GetComponent(storedEntity).WorldPosition = eventArgs.DropLocation.Position; - } - - return true; - } } /// diff --git a/Resources/Locale/en-US/storage/components/dumpable-component.ftl b/Resources/Locale/en-US/storage/components/dumpable-component.ftl new file mode 100644 index 0000000000..732a45a3b6 --- /dev/null +++ b/Resources/Locale/en-US/storage/components/dumpable-component.ftl @@ -0,0 +1,3 @@ +dump-verb-name = Dump out on ground +dump-disposal-verb-name = Dump out into {$unit} +dump-placeable-verb-name = Dump out onto {$surface} diff --git a/Resources/Locale/en-US/storage/components/storage-component.ftl b/Resources/Locale/en-US/storage/components/storage-component.ftl index e27020cd0e..0bb76bb3f0 100644 --- a/Resources/Locale/en-US/storage/components/storage-component.ftl +++ b/Resources/Locale/en-US/storage/components/storage-component.ftl @@ -1,2 +1 @@ storage-component-transfer-verb = Transfer contents -storage-component-dispose-verb = Dispose of contents \ No newline at end of file diff --git a/Resources/Prototypes/Entities/Objects/Specific/Hydroponics/tools.yml b/Resources/Prototypes/Entities/Objects/Specific/Hydroponics/tools.yml index b31bb08cf7..48e9184fcb 100644 --- a/Resources/Prototypes/Entities/Objects/Specific/Hydroponics/tools.yml +++ b/Resources/Prototypes/Entities/Objects/Specific/Hydroponics/tools.yml @@ -130,3 +130,4 @@ components: - Produce - Seed + - type: Dumpable diff --git a/Resources/Prototypes/Entities/Objects/Specific/Janitorial/trashbag.yml b/Resources/Prototypes/Entities/Objects/Specific/Janitorial/trashbag.yml index ec1cfa9f32..049d7cc857 100644 --- a/Resources/Prototypes/Entities/Objects/Specific/Janitorial/trashbag.yml +++ b/Resources/Prototypes/Entities/Objects/Specific/Janitorial/trashbag.yml @@ -29,6 +29,7 @@ - type: StorageFillVisualizer maxFillLevels: 4 fillBaseName: icon + - type: Dumpable - type: Clothing Slots: [belt] sprite: Objects/Specific/Janitorial/trashbag.rsi