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