diff --git a/Content.Server/Chemistry/EntitySystems/ReagentDispenserSystem.cs b/Content.Server/Chemistry/EntitySystems/ReagentDispenserSystem.cs
index 3cba02a599..2d751ba441 100644
--- a/Content.Server/Chemistry/EntitySystems/ReagentDispenserSystem.cs
+++ b/Content.Server/Chemistry/EntitySystems/ReagentDispenserSystem.cs
@@ -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);
diff --git a/Content.Shared/Chemistry/Components/DrainableSolutionComponent.cs b/Content.Shared/Chemistry/Components/DrainableSolutionComponent.cs
index 11768ca763..616f5a1299 100644
--- a/Content.Shared/Chemistry/Components/DrainableSolutionComponent.cs
+++ b/Content.Shared/Chemistry/Components/DrainableSolutionComponent.cs
@@ -15,4 +15,10 @@ public sealed partial class DrainableSolutionComponent : Component
///
[DataField]
public string Solution = "default";
+
+ ///
+ /// The drain doafter time required to transfer reagents from the solution.
+ ///
+ [DataField]
+ public TimeSpan DrainTime = TimeSpan.Zero;
}
diff --git a/Content.Shared/Chemistry/Components/DumpableSolutionComponent.cs b/Content.Shared/Chemistry/Components/DumpableSolutionComponent.cs
index fadf0358c2..5fab79b6b4 100644
--- a/Content.Shared/Chemistry/Components/DumpableSolutionComponent.cs
+++ b/Content.Shared/Chemistry/Components/DumpableSolutionComponent.cs
@@ -5,7 +5,9 @@ namespace Content.Shared.Chemistry.Components;
///
/// 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 .
///
[RegisterComponent, NetworkedComponent]
public sealed partial class DumpableSolutionComponent : Component
diff --git a/Content.Shared/Chemistry/Components/HyposprayComponent.cs b/Content.Shared/Chemistry/Components/HyposprayComponent.cs
index ca20e1c22f..e1e4f21101 100644
--- a/Content.Shared/Chemistry/Components/HyposprayComponent.cs
+++ b/Content.Shared/Chemistry/Components/HyposprayComponent.cs
@@ -24,6 +24,13 @@ public sealed partial class HyposprayComponent : Component
[DataField]
public FixedPoint2 TransferAmount = FixedPoint2.New(5);
+ ///
+ /// The delay to draw reagents using the hypospray.
+ /// If set, RefillTime should probably have the same value.
+ ///
+ [DataField]
+ public float DrawTime = 0f;
+
///
/// Sound that will be played when injecting.
///
diff --git a/Content.Shared/Chemistry/Components/RefillableSolutionComponent.cs b/Content.Shared/Chemistry/Components/RefillableSolutionComponent.cs
index e42bb68e61..41d6d42938 100644
--- a/Content.Shared/Chemistry/Components/RefillableSolutionComponent.cs
+++ b/Content.Shared/Chemistry/Components/RefillableSolutionComponent.cs
@@ -5,9 +5,10 @@ namespace Content.Shared.Chemistry.Components;
///
/// 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 .
///
[RegisterComponent, NetworkedComponent]
public sealed partial class RefillableSolutionComponent : Component
@@ -23,4 +24,10 @@ public sealed partial class RefillableSolutionComponent : Component
///
[DataField]
public FixedPoint2? MaxRefill = null;
+
+ ///
+ /// The refill doafter time required to transfer reagents into the solution.
+ ///
+ [DataField]
+ public TimeSpan RefillTime = TimeSpan.Zero;
}
diff --git a/Content.Shared/Chemistry/EntitySystems/HypospraySystem.cs b/Content.Shared/Chemistry/EntitySystems/HypospraySystem.cs
index 324858afd7..e179fb5f43 100644
--- a/Content.Shared/Chemistry/EntitySystems/HypospraySystem.cs
+++ b/Content.Shared/Chemistry/EntitySystems/HypospraySystem.cs
@@ -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(OnAttack);
SubscribeLocalEvent(OnUseInHand);
SubscribeLocalEvent>(AddToggleModeVerb);
+ SubscribeLocalEvent(OnDrawDoAfter);
}
#region Ref events
@@ -63,6 +66,20 @@ public sealed class HypospraySystem : EntitySystem
TryDoInject(entity, args.HitEntities[0], args.User);
}
+ private void OnDrawDoAfter(Entity 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 entity, EntityUid target, Entity targetSolution, EntityUid user)
+ public bool TryStartDraw(Entity entity, EntityUid target, Entity targetSolution, EntityUid user)
{
- if (!_solutionContainers.TryGetSolution(entity.Owner, entity.Comp.SolutionName, out var soln,
- out var solution) || solution.AvailableVolume == 0)
+ 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 entity, EntityUid target, Entity targetSolution, EntityUid user, Entity solutionEntity, [NotNullWhen(true)] out FixedPoint2? amount)
+ {
+ 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 entity, EntityUid target, Entity 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 {}
diff --git a/Content.Shared/Chemistry/EntitySystems/ScoopableSolutionSystem.cs b/Content.Shared/Chemistry/EntitySystems/ScoopableSolutionSystem.cs
index 86f9ffa390..a40c28b586 100644
--- a/Content.Shared/Chemistry/EntitySystems/ScoopableSolutionSystem.cs
+++ b/Content.Shared/Chemistry/EntitySystems/ScoopableSolutionSystem.cs
@@ -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;
diff --git a/Content.Shared/Chemistry/EntitySystems/SolutionTransferSystem.cs b/Content.Shared/Chemistry/EntitySystems/SolutionTransferSystem.cs
index b0f55a3272..4d78ab4647 100644
--- a/Content.Shared/Chemistry/EntitySystems/SolutionTransferSystem.cs
+++ b/Content.Shared/Chemistry/EntitySystems/SolutionTransferSystem.cs
@@ -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;
///
-/// Allows an entity to transfer solutions with a customizable amount per click.
-/// Also provides API for other systems.
+/// Allows an entity to transfer solutions with a customizable amount -per click-.
+/// Also provides , and API for other systems.
///
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 _refillableQuery;
+ private EntityQuery _drainableQuery;
///
/// Default transfer amounts for the set-transfer verb.
@@ -32,28 +36,18 @@ public sealed class SolutionTransferSystem : EntitySystem
base.Initialize();
SubscribeLocalEvent>(AddSetTransferVerbs);
- SubscribeLocalEvent(OnAfterInteract);
SubscribeLocalEvent(OnTransferAmountSetValueMessage);
- }
+ SubscribeLocalEvent(OnAfterInteract);
+ SubscribeLocalEvent(OnSolutionDrainTransferDoAfter);
+ SubscribeLocalEvent(OnSolutionFillTransferDoAfter);
- private void OnTransferAmountSetValueMessage(Entity 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);
-
- Dirty(uid, comp);
+ _refillableQuery = GetEntityQuery();
+ _drainableQuery = GetEntityQuery();
}
private void AddSetTransferVerbs(Entity ent, ref GetVerbsEvent 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 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 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
- //Special case for reagent tanks, because normally clicking another container will give solution, not take it.
- if (comp.CanReceive
- && !HasComp(target) // target must not be refillable (e.g. Reagent Tanks)
- && _solution.TryGetDrainableSolution(target, out var targetSoln, out _) // target must be drainable
- && TryComp(uid, out var refill)
- && _solution.TryGetRefillableSolution((uid, refill, null), out var ownerSoln, out var ownerRefill))
+ // In the case where the target has both Refillable and Drainable, Held --> Target takes priority.
+
+ 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(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 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 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 ent,
+ Entity target,
+ [NotNullWhen(true)] out Entity? drainable,
+ [NotNullWhen(true)] out Entity? refillable)
+ {
+ drainable = null;
+ refillable = null;
+
+ return ent.Comp1.CanReceive && TryGetTransferrableSolutions(ent.Owner, target, out drainable, out refillable, out _);
+ }
+
+ private bool CanRecieve(Entity ent,
+ EntityUid source,
+ [NotNullWhen(true)] out Entity? drainable,
+ [NotNullWhen(true)] out Entity? 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 source,
+ Entity target,
+ [NotNullWhen(true)] out Entity? drainable,
+ [NotNullWhen(true)] out Entity? 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;
+ }
+
///
- /// Transfer from a solution to another, allowing either entity to cancel it and show a popup.
+ /// 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
+ ///
+ /// The transfer data making up the transfer.
+ /// The actual amount transferred.
+ 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);
+ }
+
+ ///
+ /// 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.
+ ///
+ /// The transfer data making up the transfer.
+ /// The target solution,included for LoC pop-up purposes.
+ /// The actual amount transferred.
+ 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);
+ }
+
+ ///
+ /// Transfer from a solution to another, allowing either entity to cancel.
+ /// Includes a pop-up if the transfer failed.
///
/// The actual amount transferred.
- public FixedPoint2 Transfer(EntityUid user,
- EntityUid sourceEntity,
- Entity source,
- EntityUid targetEntity,
- Entity 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;
- // Check if the source is cancelling the transfer
- RaiseLocalEvent(sourceEntity, ref transferAttempt);
- if (transferAttempt.CancelReason is {} reason)
- {
- _popup.PopupClient(reason, sourceEntity, user);
+ if (!CanTransfer(data))
return FixedPoint2.Zero;
- }
- var sourceSolution = source.Comp.Solution;
- if (sourceSolution.Volume == 0)
- {
- _popup.PopupClient(Loc.GetString("comp-solution-transfer-is-empty", ("target", sourceEntity)), sourceEntity, user);
- return FixedPoint2.Zero;
- }
+ var actualAmount = FixedPoint2.Min(data.Amount, FixedPoint2.Min(sourceSolution.Volume, targetSolution.AvailableVolume));
- // Check if the target is cancelling the transfer
- RaiseLocalEvent(targetEntity, ref transferAttempt);
- if (transferAttempt.CancelReason is {} targetReason)
- {
- _popup.PopupClient(targetReason, targetEntity, user);
- return FixedPoint2.Zero;
- }
+ var solution = _solution.SplitSolution(data.Source, actualAmount);
+ _solution.AddSolution(data.Target, solution);
- var targetSolution = target.Comp.Solution;
- if (targetSolution.AvailableVolume == 0)
- {
- _popup.PopupClient(Loc.GetString("comp-solution-transfer-is-full", ("target", targetEntity)), targetEntity, user);
- return FixedPoint2.Zero;
- }
+ var ev = new SolutionTransferredEvent(data.SourceEntity, data.TargetEntity, data.User, actualAmount);
+ RaiseLocalEvent(data.TargetEntity, ref ev);
- 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);
-
- _adminLogger.Add(LogType.Action, LogImpact.Medium,
- $"{ToPrettyString(user):player} transferred {SharedSolutionContainerSystem.ToPrettyString(solution)} to {ToPrettyString(targetEntity):target}, which now contains {SharedSolutionContainerSystem.ToPrettyString(targetSolution)}");
+ _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;
}
+
+ ///
+ /// Check if the source solution can transfer the amount to the target solution, and display a pop-up if it fails.
+ ///
+ private bool CanTransfer(SolutionTransferData data)
+ {
+ var transferAttempt = new SolutionTransferAttemptEvent(data.SourceEntity, data.TargetEntity);
+
+ // Check if the source is cancelling the transfer
+ RaiseLocalEvent(data.SourceEntity, ref transferAttempt);
+ if (transferAttempt.CancelReason is {} reason)
+ {
+ _popup.PopupClient(reason, data.SourceEntity, data.User);
+ return false;
+ }
+
+ var sourceSolution = data.Source.Comp.Solution;
+ if (sourceSolution.Volume == 0)
+ {
+ _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(data.TargetEntity, ref transferAttempt);
+ if (transferAttempt.CancelReason is {} targetReason)
+ {
+ _popup.PopupClient(targetReason, data.TargetEntity, data.User);
+ return false;
+ }
+
+ var targetSolution = data.Target.Comp.Solution;
+ if (targetSolution.AvailableVolume == 0)
+ {
+ _popup.PopupClient(Loc.GetString("comp-solution-transfer-is-full", ("target", data.TargetEntity)), data.TargetEntity, data.User);
+ return false;
+ }
+
+ return true;
+ }
+}
+
+
+///
+/// A collection of data containing relevant entities and values for transferring reagents.
+///
+/// The user performing the transfer.
+/// The entity holding the solution container which reagents are being moved from.
+/// The entity holding the solution from which reagents are being moved away from.
+/// The entity holding the solution container which reagents are being moved to.
+/// The entity holding the solution which reagents are being moved to
+/// The amount being moved.
+public struct SolutionTransferData(EntityUid user, EntityUid sourceEntity, Entity source, EntityUid targetEntity, Entity target, FixedPoint2 amount)
+{
+ public EntityUid User = user;
+ public EntityUid SourceEntity = sourceEntity;
+ public Entity Source = source;
+ public EntityUid TargetEntity = targetEntity;
+ public Entity Target = target;
+ public FixedPoint2 Amount = amount;
}
///
@@ -234,3 +412,35 @@ public record struct SolutionTransferAttemptEvent(EntityUid From, EntityUid To,
///
[ByRefEvent]
public record struct SolutionTransferredEvent(EntityUid From, EntityUid To, EntityUid User, FixedPoint2 Amount);
+
+///
+/// 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.
+///
+[Serializable, NetSerializable]
+public sealed partial class SolutionDrainTransferDoAfterEvent : DoAfterEvent
+{
+ public FixedPoint2 Amount;
+
+ public SolutionDrainTransferDoAfterEvent(FixedPoint2 amount)
+ {
+ Amount = amount;
+ }
+
+ public override DoAfterEvent Clone() => this;
+}
+
+///
+/// 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.
+///
+[Serializable, NetSerializable]
+public sealed partial class SolutionRefillTransferDoAfterEvent : DoAfterEvent
+{
+ public FixedPoint2 Amount;
+
+ public SolutionRefillTransferDoAfterEvent(FixedPoint2 amount)
+ {
+ Amount = amount;
+ }
+
+ public override DoAfterEvent Clone() => this;
+}
diff --git a/Content.Shared/Fluids/EntitySystems/SolutionDumpingSystem.cs b/Content.Shared/Fluids/EntitySystems/SolutionDumpingSystem.cs
index 87873d50e7..b2bbcb2942 100644
--- a/Content.Shared/Fluids/EntitySystems/SolutionDumpingSystem.cs
+++ b/Content.Shared/Fluids/EntitySystems/SolutionDumpingSystem.cs
@@ -44,7 +44,6 @@ public sealed class SolutionDumpingSystem : EntitySystem
//SubscribeLocalEvent(OnRefillableDragged); For if you want to refill a container by dragging it into another one. Can't find a use for that currently.
SubscribeLocalEvent(OnDrainableDragged);
- SubscribeLocalEvent(OnDrainedToRefillableDragged);
SubscribeLocalEvent(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 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 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,
diff --git a/Resources/Prototypes/Entities/Objects/Specific/Medical/hypospray.yml b/Resources/Prototypes/Entities/Objects/Specific/Medical/hypospray.yml
index bc0cf16e8e..17909d6769 100644
--- a/Resources/Prototypes/Entities/Objects/Specific/Medical/hypospray.yml
+++ b/Resources/Prototypes/Entities/Objects/Specific/Medical/hypospray.yml
@@ -661,12 +661,15 @@
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