]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Add doafter to filling the hypopen (#40538)
authorSlamBamActionman <83650252+SlamBamActionman@users.noreply.github.com>
Tue, 21 Oct 2025 22:05:44 +0000 (00:05 +0200)
committerGitHub <noreply@github.com>
Tue, 21 Oct 2025 22:05:44 +0000 (22:05 +0000)
* Initial commit

* Small QOL buff

* Review changes

* Ch-ch-ch-ch-chaaaanges

* Review changes

* oops

* Oh ya fix the fill thing

* cleanup warnings make a few more private methods

---------

Co-authored-by: Princess Cheeseballs <66055347+Pronana@users.noreply.github.com>
Content.Server/Chemistry/EntitySystems/ReagentDispenserSystem.cs
Content.Shared/Chemistry/Components/DrainableSolutionComponent.cs
Content.Shared/Chemistry/Components/DumpableSolutionComponent.cs
Content.Shared/Chemistry/Components/HyposprayComponent.cs
Content.Shared/Chemistry/Components/RefillableSolutionComponent.cs
Content.Shared/Chemistry/EntitySystems/HypospraySystem.cs
Content.Shared/Chemistry/EntitySystems/ScoopableSolutionSystem.cs
Content.Shared/Chemistry/EntitySystems/SolutionTransferSystem.cs
Content.Shared/Fluids/EntitySystems/SolutionDumpingSystem.cs
Resources/Prototypes/Entities/Objects/Specific/Medical/hypospray.yml

index 3cba02a5994e19459220c8ace7b47cba03ac4429..2d751ba4418bd273f5b0dbb325f03698b9d1db11 100644 (file)
@@ -146,10 +146,10 @@ namespace Content.Server.Chemistry.EntitySystems
             {
                 // force open container, if applicable, to avoid confusing people on why it doesn't dispense
                 _openable.SetOpen(storedContainer, true);
-                _solutionTransferSystem.Transfer(reagentDispenser,
+                _solutionTransferSystem.Transfer(new SolutionTransferData(reagentDispenser,
                         storedContainer, src.Value,
                         outputContainer.Value, dst.Value,
-                        (int)reagentDispenser.Comp.DispenseAmount);
+                        (int)reagentDispenser.Comp.DispenseAmount));
             }
 
             UpdateUiState(reagentDispenser);
index 11768ca76302184d0fcba4865073045a1f18efe2..616f5a12998b0d8a32018ce0a2b41e07bb65d41b 100644 (file)
@@ -15,4 +15,10 @@ public sealed partial class DrainableSolutionComponent : Component
     /// </summary>
     [DataField]
     public string Solution = "default";
+
+    /// <summary>
+    /// The drain doafter time required to transfer reagents from the solution.
+    /// </summary>
+    [DataField]
+    public TimeSpan DrainTime = TimeSpan.Zero;
 }
index fadf0358c2c78215a1e4f68d17bfc04832f4baa1..5fab79b6b4292f5a63716ebcebc52e4e2158c886 100644 (file)
@@ -5,7 +5,9 @@ namespace Content.Shared.Chemistry.Components;
 /// <summary>
 /// Denotes that there is a solution contained in this entity that can be
 /// easily dumped into (that is, completely removed from the dumping container
-/// into this one). Think pouring a container fully into this.
+/// into this one). Think pouring a container fully into this. The action for this is represented via drag & drop.
+///
+/// To represent it being possible to controllably pour volumes into the entity, see <see cref="RefillableSolutionComponent"/>.
 /// </summary>
 [RegisterComponent, NetworkedComponent]
 public sealed partial class DumpableSolutionComponent : Component
index ca20e1c22fb6b281363b81d77165e7bbff36baf2..e1e4f211019ce2e2f3e9471ef1fbc83ea13e962d 100644 (file)
@@ -24,6 +24,13 @@ public sealed partial class HyposprayComponent : Component
     [DataField]
     public FixedPoint2 TransferAmount = FixedPoint2.New(5);
 
+    /// <summary>
+    /// The delay to draw reagents using the hypospray.
+    /// If set, <see cref="RefillableSolutionComponent"/> RefillTime should probably have the same value.
+    /// </summary>
+    [DataField]
+    public float DrawTime = 0f;
+
     /// <summary>
     ///     Sound that will be played when injecting.
     /// </summary>
index e42bb68e619bf99409294d49a41d0104684c9706..41d6d429382a89ce26b8885f7a561e01e6a03a89 100644 (file)
@@ -5,9 +5,10 @@ namespace Content.Shared.Chemistry.Components;
 
 /// <summary>
 /// Denotes that the entity has a solution contained which can be easily added
-/// to. This should go on things that are meant to be refilled, including
-/// pouring things into a beaker. If you run it under a sink tap, it's probably
-/// refillable.
+/// to in controlled volumes. This should go on things that are meant to be refilled, including
+/// pouring things into a beaker. The action for this is represented via clicking.
+///
+/// To represent it being possible to just dump entire volumes at once into an entity, see <see cref="DumpableSolutionComponent"/>.
 /// </summary>
 [RegisterComponent, NetworkedComponent]
 public sealed partial class RefillableSolutionComponent : Component
@@ -23,4 +24,10 @@ public sealed partial class RefillableSolutionComponent : Component
     /// </summary>
     [DataField]
     public FixedPoint2? MaxRefill = null;
+
+    /// <summary>
+    /// The refill doafter time required to transfer reagents into the solution.
+    /// </summary>
+    [DataField]
+    public TimeSpan RefillTime = TimeSpan.Zero;
 }
index 324858afd7bd17a05fc7f7890fa2606c724060d8..e179fb5f433ab6cd6a2ebb16f2123370d52d98ee 100644 (file)
@@ -1,10 +1,10 @@
+using System.Diagnostics.CodeAnalysis;
 using Content.Shared.Administration.Logs;
-using Content.Shared.Body.Components;
-using Content.Shared.Chemistry.EntitySystems;
 using Content.Shared.Chemistry.Components;
 using Content.Shared.Chemistry.Components.SolutionManager;
 using Content.Shared.Chemistry.Hypospray.Events;
 using Content.Shared.Database;
+using Content.Shared.DoAfter;
 using Content.Shared.FixedPoint;
 using Content.Shared.Forensics;
 using Content.Shared.IdentityManagement;
@@ -16,6 +16,7 @@ using Content.Shared.Timing;
 using Content.Shared.Verbs;
 using Content.Shared.Weapons.Melee.Events;
 using Robust.Shared.Audio.Systems;
+using Robust.Shared.Serialization;
 
 namespace Content.Shared.Chemistry.EntitySystems;
 
@@ -27,6 +28,7 @@ public sealed class HypospraySystem : EntitySystem
     [Dependency] private readonly SharedPopupSystem _popup = default!;
     [Dependency] private readonly SharedSolutionContainerSystem _solutionContainers = default!;
     [Dependency] private readonly UseDelaySystem _useDelay = default!;
+    [Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
 
     public override void Initialize()
     {
@@ -36,6 +38,7 @@ public sealed class HypospraySystem : EntitySystem
         SubscribeLocalEvent<HyposprayComponent, MeleeHitEvent>(OnAttack);
         SubscribeLocalEvent<HyposprayComponent, UseInHandEvent>(OnUseInHand);
         SubscribeLocalEvent<HyposprayComponent, GetVerbsEvent<AlternativeVerb>>(AddToggleModeVerb);
+        SubscribeLocalEvent<HyposprayComponent, HyposprayDrawDoAfterEvent>(OnDrawDoAfter);
     }
 
     #region Ref events
@@ -63,6 +66,20 @@ public sealed class HypospraySystem : EntitySystem
         TryDoInject(entity, args.HitEntities[0], args.User);
     }
 
+    private void OnDrawDoAfter(Entity<HyposprayComponent> entity, ref HyposprayDrawDoAfterEvent args)
+    {
+        if (args.Cancelled)
+            return;
+
+        if (entity.Comp.CanContainerDraw
+            && args.Target.HasValue
+            && !EligibleEntity(args.Target.Value, entity)
+            && _solutionContainers.TryGetDrawableSolution(args.Target.Value, out var drawableSolution, out _))
+        {
+            TryDraw(entity, args.Target.Value, drawableSolution.Value, args.User);
+        }
+    }
+
     #endregion
 
     #region Draw/Inject
@@ -73,7 +90,7 @@ public sealed class HypospraySystem : EntitySystem
             && !EligibleEntity(target, entity)
             && _solutionContainers.TryGetDrawableSolution(target, out var drawableSolution, out _))
         {
-            return TryDraw(entity, target, drawableSolution.Value, user);
+            return TryStartDraw(entity, target, drawableSolution.Value, user);
         }
 
         return TryDoInject(entity, target, user);
@@ -186,17 +203,37 @@ public sealed class HypospraySystem : EntitySystem
         return true;
     }
 
-    private bool TryDraw(Entity<HyposprayComponent> entity, EntityUid target, Entity<SolutionComponent> targetSolution, EntityUid user)
+    public bool TryStartDraw(Entity<HyposprayComponent> entity, EntityUid target, Entity<SolutionComponent> targetSolution, EntityUid user)
+    {
+        if (!_solutionContainers.TryGetSolution(entity.Owner, entity.Comp.SolutionName, out var soln))
+            return false;
+
+        if (!TryGetDrawAmount(entity, target, targetSolution, user,  soln.Value, out _))
+            return false;
+
+        var doAfterArgs = new DoAfterArgs(EntityManager, user, entity.Comp.DrawTime, new HyposprayDrawDoAfterEvent(), entity, target)
+        {
+            BreakOnDamage = true,
+            BreakOnMove = true,
+            NeedHand = true,
+            Hidden = true,
+        };
+
+        return _doAfter.TryStartDoAfter(doAfterArgs, out _);
+    }
+
+    private bool TryGetDrawAmount(Entity<HyposprayComponent> entity, EntityUid target, Entity<SolutionComponent> targetSolution, EntityUid user, Entity<SolutionComponent> solutionEntity, [NotNullWhen(true)] out FixedPoint2? amount)
     {
-        if (!_solutionContainers.TryGetSolution(entity.Owner, entity.Comp.SolutionName, out var soln,
-                out var solution) || solution.AvailableVolume == 0)
+        amount = null;
+
+        if (solutionEntity.Comp.Solution.AvailableVolume == 0)
         {
             return false;
         }
 
         // Get transfer amount. May be smaller than _transferAmount if not enough room, also make sure there's room in the injector
         var realTransferAmount = FixedPoint2.Min(entity.Comp.TransferAmount, targetSolution.Comp.Solution.Volume,
-            solution.AvailableVolume);
+            solutionEntity.Comp.Solution.AvailableVolume);
 
         if (realTransferAmount <= 0)
         {
@@ -207,7 +244,19 @@ public sealed class HypospraySystem : EntitySystem
             return false;
         }
 
-        var removedSolution = _solutionContainers.Draw(target, targetSolution, realTransferAmount);
+        amount = realTransferAmount;
+        return true;
+    }
+
+    private bool TryDraw(Entity<HyposprayComponent> entity, EntityUid target, Entity<SolutionComponent> targetSolution, EntityUid user)
+    {
+        if (!_solutionContainers.TryGetSolution(entity.Owner, entity.Comp.SolutionName, out var soln))
+            return false;
+
+        if (!TryGetDrawAmount(entity, target, targetSolution, user, soln.Value, out var amount))
+            return false;
+
+        var removedSolution = _solutionContainers.Draw(target, targetSolution, amount.Value);
 
         if (!_solutionContainers.TryAddSolution(soln.Value, removedSolution))
         {
@@ -275,3 +324,6 @@ public sealed class HypospraySystem : EntitySystem
 
     #endregion
 }
+
+[Serializable, NetSerializable]
+public sealed partial class HyposprayDrawDoAfterEvent : SimpleDoAfterEvent {}
index 86f9ffa3909e51d44afa6b3a27c16edc5ead258f..a40c28b5866f5f3350934caf633556d5cba45810 100644 (file)
@@ -36,7 +36,7 @@ public sealed class ScoopableSolutionSystem : EntitySystem
             !_solution.TryGetRefillableSolution(beaker, out var target, out _))
             return false;
 
-        var scooped = _solutionTransfer.Transfer(user, ent, src.Value, beaker, target.Value, srcSolution.Volume);
+        var scooped = _solutionTransfer.Transfer(new SolutionTransferData(user, ent, src.Value, beaker, target.Value, srcSolution.Volume));
         if (scooped == 0)
             return false;
 
index b0f55a32720cf5b01a5ed1c762ebe5f4aa25a779..4d78ab464707b43dad3ceb115218450726d07dd3 100644 (file)
@@ -1,19 +1,19 @@
+using System.Diagnostics.CodeAnalysis;
 using Content.Shared.Administration.Logs;
-using Content.Shared.Chemistry;
 using Content.Shared.Chemistry.Components;
 using Content.Shared.Database;
+using Content.Shared.DoAfter;
 using Content.Shared.FixedPoint;
 using Content.Shared.Interaction;
 using Content.Shared.Popups;
 using Content.Shared.Verbs;
-using Robust.Shared.Network;
-using Robust.Shared.Player;
+using Robust.Shared.Serialization;
 
 namespace Content.Shared.Chemistry.EntitySystems;
 
 /// <summary>
-/// Allows an entity to transfer solutions with a customizable amount per click.
-/// Also provides <see cref="Transfer"/> API for other systems.
+/// Allows an entity to transfer solutions with a customizable amount -per click-.
+/// Also provides <see cref="Transfer"/>, <see cref="RefillTransfer"/> and <see cref="DrainTransfer"/> API for other systems.
 /// </summary>
 public sealed class SolutionTransferSystem : EntitySystem
 {
@@ -21,6 +21,10 @@ public sealed class SolutionTransferSystem : EntitySystem
     [Dependency] private readonly SharedPopupSystem _popup = default!;
     [Dependency] private readonly SharedSolutionContainerSystem _solution = default!;
     [Dependency] private readonly SharedUserInterfaceSystem _ui = default!;
+    [Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
+
+    private EntityQuery<RefillableSolutionComponent> _refillableQuery;
+    private EntityQuery<DrainableSolutionComponent> _drainableQuery;
 
     /// <summary>
     ///     Default transfer amounts for the set-transfer verb.
@@ -32,28 +36,18 @@ public sealed class SolutionTransferSystem : EntitySystem
         base.Initialize();
 
         SubscribeLocalEvent<SolutionTransferComponent, GetVerbsEvent<AlternativeVerb>>(AddSetTransferVerbs);
-        SubscribeLocalEvent<SolutionTransferComponent, AfterInteractEvent>(OnAfterInteract);
         SubscribeLocalEvent<SolutionTransferComponent, TransferAmountSetValueMessage>(OnTransferAmountSetValueMessage);
-    }
-
-    private void OnTransferAmountSetValueMessage(Entity<SolutionTransferComponent> ent, ref TransferAmountSetValueMessage message)
-    {
-        var (uid, comp) = ent;
-
-        var newTransferAmount = FixedPoint2.Clamp(message.Value, comp.MinimumTransferAmount, comp.MaximumTransferAmount);
-        comp.TransferAmount = newTransferAmount;
-
-        if (message.Actor is { Valid: true } user)
-            _popup.PopupEntity(Loc.GetString("comp-solution-transfer-set-amount", ("amount", newTransferAmount)), uid, user);
+        SubscribeLocalEvent<SolutionTransferComponent, AfterInteractEvent>(OnAfterInteract);
+        SubscribeLocalEvent<SolutionTransferComponent, SolutionDrainTransferDoAfterEvent>(OnSolutionDrainTransferDoAfter);
+        SubscribeLocalEvent<SolutionTransferComponent, SolutionRefillTransferDoAfterEvent>(OnSolutionFillTransferDoAfter);
 
-        Dirty(uid, comp);
+        _refillableQuery = GetEntityQuery<RefillableSolutionComponent>();
+        _drainableQuery = GetEntityQuery<DrainableSolutionComponent>();
     }
 
     private void AddSetTransferVerbs(Entity<SolutionTransferComponent> ent, ref GetVerbsEvent<AlternativeVerb> args)
     {
-        var (uid, comp) = ent;
-
-        if (!args.CanAccess || !args.CanInteract || !comp.CanChangeTransferAmount || args.Hands == null)
+        if (!args.CanAccess || !args.CanInteract || !ent.Comp.CanChangeTransferAmount || args.Hands == null)
             return;
 
         // Custom transfer verb
@@ -66,7 +60,7 @@ public sealed class SolutionTransferSystem : EntitySystem
             // TODO: remove server check when bui prediction is a thing
             Act = () =>
             {
-                _ui.OpenUi(uid, TransferAmountUiKey.Key, @event.User);
+                _ui.OpenUi(ent.Owner, TransferAmountUiKey.Key, @event.User);
             },
             Priority = 1
         });
@@ -76,7 +70,7 @@ public sealed class SolutionTransferSystem : EntitySystem
         var user = args.User;
         foreach (var amount in DefaultTransferAmounts)
         {
-          if (amount < comp.MinimumTransferAmount || amount > comp.MaximumTransferAmount)
+            if (amount < ent.Comp.MinimumTransferAmount || amount > ent.Comp.MaximumTransferAmount)
                 continue;
 
             AlternativeVerb verb = new();
@@ -84,11 +78,11 @@ public sealed class SolutionTransferSystem : EntitySystem
             verb.Category = VerbCategory.SetTransferAmount;
             verb.Act = () =>
             {
-                comp.TransferAmount = amount;
+                ent.Comp.TransferAmount = amount;
 
-                _popup.PopupClient(Loc.GetString("comp-solution-transfer-set-amount", ("amount", amount)), uid, user);
+                _popup.PopupClient(Loc.GetString("comp-solution-transfer-set-amount", ("amount", amount)), ent.Owner, user);
 
-                Dirty(uid, comp);
+                Dirty(ent.Owner, ent.Comp);
             };
 
             // we want to sort by size, not alphabetically by the verb text.
@@ -99,117 +93,301 @@ public sealed class SolutionTransferSystem : EntitySystem
         }
     }
 
+    private void OnTransferAmountSetValueMessage(Entity<SolutionTransferComponent> ent, ref TransferAmountSetValueMessage message)
+    {
+        var newTransferAmount = FixedPoint2.Clamp(message.Value, ent.Comp.MinimumTransferAmount, ent.Comp.MaximumTransferAmount);
+        ent.Comp.TransferAmount = newTransferAmount;
+
+        if (message.Actor is { Valid: true } user)
+            _popup.PopupEntity(Loc.GetString("comp-solution-transfer-set-amount", ("amount", newTransferAmount)), ent.Owner, user);
+
+        Dirty(ent.Owner, ent.Comp);
+    }
+
     private void OnAfterInteract(Entity<SolutionTransferComponent> ent, ref AfterInteractEvent args)
     {
         if (!args.CanReach || args.Target is not {} target)
             return;
 
-        var (uid, comp) = ent;
+        // We have two cases for interaction:
+        // Held Drainable --> Target Refillable
+        // Held Refillable <-- Target Drainable
+
+        // In the case where the target has both Refillable and Drainable, Held --> Target takes priority.
 
-        //Special case for reagent tanks, because normally clicking another container will give solution, not take it.
-        if (comp.CanReceive
-            && !HasComp<RefillableSolutionComponent>(target) // target must not be refillable (e.g. Reagent Tanks)
-            && _solution.TryGetDrainableSolution(target, out var targetSoln, out _) // target must be drainable
-            && TryComp<RefillableSolutionComponent>(uid, out var refill)
-            && _solution.TryGetRefillableSolution((uid, refill, null), out var ownerSoln, out var ownerRefill))
+        if (ent.Comp.CanSend
+            && _drainableQuery.TryComp(ent.Owner, out var heldDrainable)
+            && _refillableQuery.TryComp(target, out var targetRefillable)
+            && TryGetTransferrableSolutions((ent.Owner, heldDrainable),
+                (target, targetRefillable),
+                out var ownerSoln,
+                out var targetSoln,
+                out _))
         {
-            var transferAmount = comp.TransferAmount; // This is the player-configurable transfer amount of "uid," not the target reagent tank.
+            args.Handled = true; //If we reach this point, the interaction counts as handled.
 
-            // if the receiver has a smaller transfer limit, use that instead
-            if (refill?.MaxRefill is {} maxRefill)
+            var transferAmount = ent.Comp.TransferAmount;
+            if (targetRefillable.MaxRefill is {} maxRefill)
                 transferAmount = FixedPoint2.Min(transferAmount, maxRefill);
 
-            var transferred = Transfer(args.User, target, targetSoln.Value, uid, ownerSoln.Value, transferAmount);
-            args.Handled = true;
-            if (transferred > 0)
-            {
-                var toTheBrim = ownerRefill.AvailableVolume == 0;
-                var msg = toTheBrim
-                    ? "comp-solution-transfer-fill-fully"
-                    : "comp-solution-transfer-fill-normal";
+            var transferData = new SolutionTransferData(args.User, ent.Owner, ownerSoln.Value, target, targetSoln.Value, transferAmount);
+            var transferTime = targetRefillable.RefillTime + heldDrainable.DrainTime;
 
-                _popup.PopupClient(Loc.GetString(msg, ("owner", args.Target), ("amount", transferred), ("target", uid)), uid, args.User);
-                return;
+            if (transferTime > TimeSpan.Zero)
+            {
+                if (!CanTransfer(transferData))
+                    return;
+
+                var doAfterArgs = new DoAfterArgs(EntityManager, args.User, transferTime, new SolutionDrainTransferDoAfterEvent(transferAmount), ent.Owner, target)
+                {
+                    BreakOnDamage = true,
+                    BreakOnMove = true,
+                    NeedHand = true,
+                    Hidden = true,
+                };
+                _doAfter.TryStartDoAfter(doAfterArgs);
+            }
+            else
+            {
+                DrainTransfer(transferData);
             }
+
+            return;
         }
 
-        // if target is refillable, and owner is drainable
-        if (comp.CanSend
-            && TryComp<RefillableSolutionComponent>(target, out var targetRefill)
-            && _solution.TryGetRefillableSolution((target, targetRefill, null), out targetSoln, out _)
-            && _solution.TryGetDrainableSolution(uid, out ownerSoln, out _))
+        if (ent.Comp.CanReceive
+            && _refillableQuery.TryComp(ent.Owner, out var heldRefillable)
+            && _drainableQuery.TryComp(target, out var targetDrainable)
+            && TryGetTransferrableSolutions((target, targetDrainable),
+                (ent.Owner, heldRefillable),
+                out targetSoln,
+                out ownerSoln,
+                out var solution))
         {
-            var transferAmount = comp.TransferAmount;
+            args.Handled = true; //If we reach this point, the interaction counts as handled.
 
-            if (targetRefill?.MaxRefill is {} maxRefill)
+            var transferAmount = ent.Comp.TransferAmount; // This is the player-configurable transfer amount of "uid," not the target drainable.
+            if (heldRefillable.MaxRefill is {} maxRefill) // if the receiver has a smaller transfer limit, use that instead
                 transferAmount = FixedPoint2.Min(transferAmount, maxRefill);
 
-            var transferred = Transfer(args.User, uid, ownerSoln.Value, target, targetSoln.Value, transferAmount);
-            args.Handled = true;
-            if (transferred > 0)
+            var transferData = new SolutionTransferData(args.User, target, targetSoln.Value, ent.Owner, ownerSoln.Value, transferAmount);
+            var transferTime = heldRefillable.RefillTime + targetDrainable.DrainTime;
+
+            if (transferTime > TimeSpan.Zero)
             {
-                var message = Loc.GetString("comp-solution-transfer-transfer-solution", ("amount", transferred), ("target", target));
-                _popup.PopupClient(message, uid, args.User);
+                if (!CanTransfer(transferData))
+                    return;
+
+                var doAfterArgs = new DoAfterArgs(EntityManager, args.User, transferTime, new SolutionRefillTransferDoAfterEvent(transferAmount), ent.Owner, target)
+                {
+                    BreakOnDamage = true,
+                    BreakOnMove = true,
+                    NeedHand = true,
+                    Hidden = true,
+                };
+                _doAfter.TryStartDoAfter(doAfterArgs);
+            }
+            else
+            {
+                RefillTransfer(transferData, solution);
             }
         }
     }
 
+    private void OnSolutionDrainTransferDoAfter(Entity<SolutionTransferComponent> ent, ref SolutionDrainTransferDoAfterEvent args)
+    {
+        if (args.Cancelled || args.Target is not { } target)
+            return;
+
+        // Have to check again, in case something has changed.
+        if (CanSend(ent, target, out var ownerSoln, out var targetSoln))
+        {
+            DrainTransfer(new SolutionTransferData(args.User, ent.Owner, ownerSoln.Value, args.Target.Value, targetSoln.Value, args.Amount));
+        }
+    }
+
+    private void OnSolutionFillTransferDoAfter(Entity<SolutionTransferComponent> ent, ref SolutionRefillTransferDoAfterEvent args)
+    {
+        if (args.Cancelled || args.Target is not { } target)
+            return;
+
+        // Have to check again, in case something has changed.
+        if (!CanRecieve(ent, target, out var ownerSoln, out var targetSoln, out var solution))
+            return;
+
+        RefillTransfer(new SolutionTransferData(args.User, target, targetSoln.Value, ent.Owner, ownerSoln.Value, args.Amount), solution);
+    }
+
+    private bool CanSend(Entity<SolutionTransferComponent, DrainableSolutionComponent?> ent,
+        Entity<RefillableSolutionComponent?> target,
+        [NotNullWhen(true)] out Entity<SolutionComponent>? drainable,
+        [NotNullWhen(true)] out Entity<SolutionComponent>? refillable)
+    {
+        drainable = null;
+        refillable = null;
+
+        return ent.Comp1.CanReceive && TryGetTransferrableSolutions(ent.Owner, target, out drainable, out refillable, out _);
+    }
+
+    private bool CanRecieve(Entity<SolutionTransferComponent> ent,
+        EntityUid source,
+        [NotNullWhen(true)] out Entity<SolutionComponent>? drainable,
+        [NotNullWhen(true)] out Entity<SolutionComponent>? refillable,
+        [NotNullWhen(true)] out Solution? solution)
+    {
+        drainable = null;
+        refillable = null;
+        solution = null;
+
+        return ent.Comp.CanReceive && TryGetTransferrableSolutions(source, ent.Owner, out drainable, out refillable, out solution);
+    }
+
+    private bool TryGetTransferrableSolutions(Entity<DrainableSolutionComponent?> source,
+        Entity<RefillableSolutionComponent?> target,
+        [NotNullWhen(true)] out Entity<SolutionComponent>? drainable,
+        [NotNullWhen(true)] out Entity<SolutionComponent>? refillable,
+        [NotNullWhen(true)] out Solution? solution)
+    {
+        drainable = null;
+        refillable = null;
+        solution = null;
+
+        if (!_drainableQuery.Resolve(source, ref source.Comp) || !_refillableQuery.Resolve(target, ref target.Comp))
+            return false;
+
+        if (!_solution.TryGetDrainableSolution(source, out drainable, out _))
+            return false;
+
+        if (!_solution.TryGetRefillableSolution(target, out refillable, out solution))
+            return false;
+
+        return true;
+    }
+
+    /// <summary>
+    /// Attempt to drain a solution into another, such as pouring a bottle into a glass.
+    /// Includes a pop-up if the transfer failed or succeeded
+    /// </summary>
+    /// <param name="data">The transfer data making up the transfer.</param>
+    /// <returns>The actual amount transferred.</returns>
+    private void DrainTransfer(SolutionTransferData data)
+    {
+        var transferred = Transfer(data);
+        if (transferred <= 0)
+            return;
+
+        var message = Loc.GetString("comp-solution-transfer-transfer-solution", ("amount", transferred), ("target", data.TargetEntity));
+        _popup.PopupClient(message, data.SourceEntity, data.User);
+    }
+
+    /// <summary>
+    /// Attempt to fill a solution from another container, such as tapping from a water tank.
+    /// Includes a pop-up if the transfer failed or succeeded.
+    /// </summary>
+    /// <param name="data">The transfer data making up the transfer.</param>
+    /// <param name="targetSolution">The target solution,included for LoC pop-up purposes.</param>
+    /// <returns>The actual amount transferred.</returns>
+    private void RefillTransfer(SolutionTransferData data, Solution targetSolution)
+    {
+        var transferred = Transfer(data);
+        if (transferred <= 0)
+            return;
+
+        var toTheBrim = targetSolution.AvailableVolume == 0;
+        var msg = toTheBrim
+            ? "comp-solution-transfer-fill-fully"
+            : "comp-solution-transfer-fill-normal";
+
+        _popup.PopupClient(Loc.GetString(msg, ("owner", data.SourceEntity), ("amount", transferred), ("target", data.TargetEntity)), data.TargetEntity, data.User);
+    }
+
     /// <summary>
-    /// Transfer from a solution to another, allowing either entity to cancel it and show a popup.
+    /// Transfer from a solution to another, allowing either entity to cancel.
+    /// Includes a pop-up if the transfer failed.
     /// </summary>
     /// <returns>The actual amount transferred.</returns>
-    public FixedPoint2 Transfer(EntityUid user,
-        EntityUid sourceEntity,
-        Entity<SolutionComponent> source,
-        EntityUid targetEntity,
-        Entity<SolutionComponent> target,
-        FixedPoint2 amount)
+    public FixedPoint2 Transfer(SolutionTransferData data)
     {
-        var transferAttempt = new SolutionTransferAttemptEvent(sourceEntity, targetEntity);
+        var sourceSolution = data.Source.Comp.Solution;
+        var targetSolution = data.Target.Comp.Solution;
+
+        if (!CanTransfer(data))
+            return FixedPoint2.Zero;
+
+        var actualAmount = FixedPoint2.Min(data.Amount, FixedPoint2.Min(sourceSolution.Volume, targetSolution.AvailableVolume));
+
+        var solution = _solution.SplitSolution(data.Source, actualAmount);
+        _solution.AddSolution(data.Target, solution);
+
+        var ev = new SolutionTransferredEvent(data.SourceEntity, data.TargetEntity, data.User, actualAmount);
+        RaiseLocalEvent(data.TargetEntity, ref ev);
+
+        _adminLogger.Add(LogType.Action,
+            LogImpact.Medium,
+            $"{ToPrettyString(data.User):player} transferred {SharedSolutionContainerSystem.ToPrettyString(solution)} to {ToPrettyString(data.TargetEntity):target}, which now contains {SharedSolutionContainerSystem.ToPrettyString(targetSolution)}");
+
+        return actualAmount;
+    }
+
+    /// <summary>
+    /// Check if the source solution can transfer the amount to the target solution, and display a pop-up if it fails.
+    /// </summary>
+    private bool CanTransfer(SolutionTransferData data)
+    {
+        var transferAttempt = new SolutionTransferAttemptEvent(data.SourceEntity, data.TargetEntity);
 
         // Check if the source is cancelling the transfer
-        RaiseLocalEvent(sourceEntity, ref transferAttempt);
+        RaiseLocalEvent(data.SourceEntity, ref transferAttempt);
         if (transferAttempt.CancelReason is {} reason)
         {
-            _popup.PopupClient(reason, sourceEntity, user);
-            return FixedPoint2.Zero;
+            _popup.PopupClient(reason, data.SourceEntity, data.User);
+            return false;
         }
 
-        var sourceSolution = source.Comp.Solution;
+        var sourceSolution = data.Source.Comp.Solution;
         if (sourceSolution.Volume == 0)
         {
-            _popup.PopupClient(Loc.GetString("comp-solution-transfer-is-empty", ("target", sourceEntity)), sourceEntity, user);
-            return FixedPoint2.Zero;
+            _popup.PopupClient(Loc.GetString("comp-solution-transfer-is-empty", ("target", data.SourceEntity)), data.SourceEntity, data.User);
+            return false;
         }
 
         // Check if the target is cancelling the transfer
-        RaiseLocalEvent(targetEntity, ref transferAttempt);
+        RaiseLocalEvent(data.TargetEntity, ref transferAttempt);
         if (transferAttempt.CancelReason is {} targetReason)
         {
-            _popup.PopupClient(targetReason, targetEntity, user);
-            return FixedPoint2.Zero;
+            _popup.PopupClient(targetReason, data.TargetEntity, data.User);
+            return false;
         }
 
-        var targetSolution = target.Comp.Solution;
+        var targetSolution = data.Target.Comp.Solution;
         if (targetSolution.AvailableVolume == 0)
         {
-            _popup.PopupClient(Loc.GetString("comp-solution-transfer-is-full", ("target", targetEntity)), targetEntity, user);
-            return FixedPoint2.Zero;
+            _popup.PopupClient(Loc.GetString("comp-solution-transfer-is-full", ("target", data.TargetEntity)), data.TargetEntity, data.User);
+            return false;
         }
 
-        var actualAmount = FixedPoint2.Min(amount, FixedPoint2.Min(sourceSolution.Volume, targetSolution.AvailableVolume));
-
-        var solution = _solution.SplitSolution(source, actualAmount);
-        _solution.AddSolution(target, solution);
-
-        var ev = new SolutionTransferredEvent(sourceEntity, targetEntity, user, actualAmount);
-        RaiseLocalEvent(targetEntity, ref ev);
+        return true;
+    }
+}
 
-        _adminLogger.Add(LogType.Action, LogImpact.Medium,
-            $"{ToPrettyString(user):player} transferred {SharedSolutionContainerSystem.ToPrettyString(solution)} to {ToPrettyString(targetEntity):target}, which now contains {SharedSolutionContainerSystem.ToPrettyString(targetSolution)}");
 
-        return actualAmount;
-    }
+/// <summary>
+/// A collection of data containing relevant entities and values for transferring reagents.
+/// </summary>
+/// <param name="user">The user performing the transfer.</param>
+/// <param name="sourceEntity">The entity holding the solution container which reagents are being moved from.</param>
+/// <param name="source">The entity holding the solution from which reagents are being moved away from.</param>
+/// <param name="targetEntity">The entity holding the solution container which reagents are being moved to.</param>
+/// <param name="target">The entity holding the solution which reagents are being moved to</param>
+/// <param name="amount">The amount being moved.</param>
+public struct SolutionTransferData(EntityUid user, EntityUid sourceEntity, Entity<SolutionComponent> source, EntityUid targetEntity, Entity<SolutionComponent> target, FixedPoint2 amount)
+{
+    public EntityUid User = user;
+    public EntityUid SourceEntity = sourceEntity;
+    public Entity<SolutionComponent> Source = source;
+    public EntityUid TargetEntity = targetEntity;
+    public Entity<SolutionComponent> Target = target;
+    public FixedPoint2 Amount = amount;
 }
 
 /// <summary>
@@ -234,3 +412,35 @@ public record struct SolutionTransferAttemptEvent(EntityUid From, EntityUid To,
 /// </summary>
 [ByRefEvent]
 public record struct SolutionTransferredEvent(EntityUid From, EntityUid To, EntityUid User, FixedPoint2 Amount);
+
+/// <summary>
+/// Doafter event for solution transfers where the held item is drained into the target. Checks for validity both when initiating and when finishing the event.
+/// </summary>
+[Serializable, NetSerializable]
+public sealed partial class SolutionDrainTransferDoAfterEvent : DoAfterEvent
+{
+    public FixedPoint2 Amount;
+
+    public SolutionDrainTransferDoAfterEvent(FixedPoint2 amount)
+    {
+        Amount = amount;
+    }
+
+    public override DoAfterEvent Clone() => this;
+}
+
+/// <summary>
+/// Doafter event for solution transfers where the held item is filled from the target. Checks for validity both when initiating and when finishing the event.
+/// </summary>
+[Serializable, NetSerializable]
+public sealed partial class SolutionRefillTransferDoAfterEvent : DoAfterEvent
+{
+    public FixedPoint2 Amount;
+
+    public SolutionRefillTransferDoAfterEvent(FixedPoint2 amount)
+    {
+        Amount = amount;
+    }
+
+    public override DoAfterEvent Clone() => this;
+}
index 87873d50e7f4195aa7a3351ae0a7635a1629a7e8..b2bbcb2942e7729a543493116a6927265cb21e72 100644 (file)
@@ -44,7 +44,6 @@ public sealed class SolutionDumpingSystem : EntitySystem
         //SubscribeLocalEvent<RefillableSolutionComponent, DragDropDraggedEvent>(OnRefillableDragged); For if you want to refill a container by dragging it into another one. Can't find a use for that currently.
         SubscribeLocalEvent<DrainableSolutionComponent, DragDropDraggedEvent>(OnDrainableDragged);
 
-        SubscribeLocalEvent<RefillableSolutionComponent, DrainedTargetEvent>(OnDrainedToRefillableDragged);
         SubscribeLocalEvent<DumpableSolutionComponent, DrainedTargetEvent>(OnDrainedToDumpableDragged);
 
         // We use queries for these since CanDropDraggedEvent gets called pretty rapidly
@@ -62,7 +61,7 @@ public sealed class SolutionDumpingSystem : EntitySystem
     private void OnDrainableCanDragDropped(Entity<DrainableSolutionComponent> ent, ref CanDropDraggedEvent args)
     {
         // Easily drawn-from thing can be dragged onto easily refillable thing.
-        if (!_refillableQuery.HasComp(args.Target) && !_dumpQuery.HasComp(args.Target))
+        if (!_dumpQuery.HasComp(args.Target))
             return;
 
         args.CanDrop = true;
@@ -121,28 +120,6 @@ public sealed class SolutionDumpingSystem : EntitySystem
         _audio.PlayPredicted(AbsorbentComponent.DefaultTransferSound, ent, args.User);
     }
 
-    private void OnDrainedToRefillableDragged(Entity<RefillableSolutionComponent> ent, ref DrainedTargetEvent args)
-    {
-        if (!_solContainer.TryGetRefillableSolution((ent, ent.Comp),
-                out var targetSolEnt,
-                out var targetSol))
-            return;
-
-        // Check openness, hands, source being empty, and target being full.
-        if (!DragInteractionChecks(args.User,
-                args.Source,
-                ent.Owner,
-                args.SourceSolution,
-                targetSol,
-                out var sourceEnt))
-            return;
-
-        _solContainer.TryAddSolution(targetSolEnt.Value,
-            _solContainer.SplitSolution(sourceEnt.Value, targetSol.AvailableVolume));
-
-        _audio.PlayPredicted(AbsorbentComponent.DefaultTransferSound, ent, args.User);
-    }
-
     // Common checks between dragging handlers.
     private bool DragInteractionChecks(EntityUid user,
         EntityUid sourceContainer,
index bc0cf16e8e5a4b8b945e5b4c15a2fdc716b1ca23..17909d67690f838a472448cec42036aba17d5d80 100644 (file)
         maxVol: 10
   - type: RefillableSolution
     solution: hypospray
+    refillTime: 1.25
+    maxRefill: 5
   - type: ExaminableSolution
     solution: hypospray
     heldOnly: true # Allow examination only when held in hand.
     exactVolume: true
   - type: Hypospray
     onlyAffectsMobs: false
+    drawTime: 1.25
   - type: UseDelay
     delay: 0.5
   - type: StaticPrice # A new shitcurity meta