Add doafter to filling the hypopen (#40538)

* 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>
This commit is contained in:
SlamBamActionman
2025-10-22 00:05:44 +02:00
committed by GitHub
parent 09aada2e3e
commit c6352786f1
10 changed files with 404 additions and 140 deletions

View 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 // force open container, if applicable, to avoid confusing people on why it doesn't dispense
_openable.SetOpen(storedContainer, true); _openable.SetOpen(storedContainer, true);
_solutionTransferSystem.Transfer(reagentDispenser, _solutionTransferSystem.Transfer(new SolutionTransferData(reagentDispenser,
storedContainer, src.Value, storedContainer, src.Value,
outputContainer.Value, dst.Value, outputContainer.Value, dst.Value,
(int)reagentDispenser.Comp.DispenseAmount); (int)reagentDispenser.Comp.DispenseAmount));
} }
UpdateUiState(reagentDispenser); UpdateUiState(reagentDispenser);

View File

@@ -15,4 +15,10 @@ public sealed partial class DrainableSolutionComponent : Component
/// </summary> /// </summary>
[DataField] [DataField]
public string Solution = "default"; public string Solution = "default";
/// <summary>
/// The drain doafter time required to transfer reagents from the solution.
/// </summary>
[DataField]
public TimeSpan DrainTime = TimeSpan.Zero;
} }

View File

@@ -5,7 +5,9 @@ namespace Content.Shared.Chemistry.Components;
/// <summary> /// <summary>
/// Denotes that there is a solution contained in this entity that can be /// Denotes that there is a solution contained in this entity that can be
/// easily dumped into (that is, completely removed from the dumping container /// 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> /// </summary>
[RegisterComponent, NetworkedComponent] [RegisterComponent, NetworkedComponent]
public sealed partial class DumpableSolutionComponent : Component public sealed partial class DumpableSolutionComponent : Component

View File

@@ -24,6 +24,13 @@ public sealed partial class HyposprayComponent : Component
[DataField] [DataField]
public FixedPoint2 TransferAmount = FixedPoint2.New(5); 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> /// <summary>
/// Sound that will be played when injecting. /// Sound that will be played when injecting.
/// </summary> /// </summary>

View File

@@ -5,9 +5,10 @@ namespace Content.Shared.Chemistry.Components;
/// <summary> /// <summary>
/// Denotes that the entity has a solution contained which can be easily added /// 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 /// to in controlled volumes. 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 /// pouring things into a beaker. The action for this is represented via clicking.
/// refillable. ///
/// To represent it being possible to just dump entire volumes at once into an entity, see <see cref="DumpableSolutionComponent"/>.
/// </summary> /// </summary>
[RegisterComponent, NetworkedComponent] [RegisterComponent, NetworkedComponent]
public sealed partial class RefillableSolutionComponent : Component public sealed partial class RefillableSolutionComponent : Component
@@ -23,4 +24,10 @@ public sealed partial class RefillableSolutionComponent : Component
/// </summary> /// </summary>
[DataField] [DataField]
public FixedPoint2? MaxRefill = null; public FixedPoint2? MaxRefill = null;
/// <summary>
/// The refill doafter time required to transfer reagents into the solution.
/// </summary>
[DataField]
public TimeSpan RefillTime = TimeSpan.Zero;
} }

View File

@@ -1,10 +1,10 @@
using System.Diagnostics.CodeAnalysis;
using Content.Shared.Administration.Logs; 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;
using Content.Shared.Chemistry.Components.SolutionManager; using Content.Shared.Chemistry.Components.SolutionManager;
using Content.Shared.Chemistry.Hypospray.Events; using Content.Shared.Chemistry.Hypospray.Events;
using Content.Shared.Database; using Content.Shared.Database;
using Content.Shared.DoAfter;
using Content.Shared.FixedPoint; using Content.Shared.FixedPoint;
using Content.Shared.Forensics; using Content.Shared.Forensics;
using Content.Shared.IdentityManagement; using Content.Shared.IdentityManagement;
@@ -16,6 +16,7 @@ using Content.Shared.Timing;
using Content.Shared.Verbs; using Content.Shared.Verbs;
using Content.Shared.Weapons.Melee.Events; using Content.Shared.Weapons.Melee.Events;
using Robust.Shared.Audio.Systems; using Robust.Shared.Audio.Systems;
using Robust.Shared.Serialization;
namespace Content.Shared.Chemistry.EntitySystems; namespace Content.Shared.Chemistry.EntitySystems;
@@ -27,6 +28,7 @@ public sealed class HypospraySystem : EntitySystem
[Dependency] private readonly SharedPopupSystem _popup = default!; [Dependency] private readonly SharedPopupSystem _popup = default!;
[Dependency] private readonly SharedSolutionContainerSystem _solutionContainers = default!; [Dependency] private readonly SharedSolutionContainerSystem _solutionContainers = default!;
[Dependency] private readonly UseDelaySystem _useDelay = default!; [Dependency] private readonly UseDelaySystem _useDelay = default!;
[Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
public override void Initialize() public override void Initialize()
{ {
@@ -36,6 +38,7 @@ public sealed class HypospraySystem : EntitySystem
SubscribeLocalEvent<HyposprayComponent, MeleeHitEvent>(OnAttack); SubscribeLocalEvent<HyposprayComponent, MeleeHitEvent>(OnAttack);
SubscribeLocalEvent<HyposprayComponent, UseInHandEvent>(OnUseInHand); SubscribeLocalEvent<HyposprayComponent, UseInHandEvent>(OnUseInHand);
SubscribeLocalEvent<HyposprayComponent, GetVerbsEvent<AlternativeVerb>>(AddToggleModeVerb); SubscribeLocalEvent<HyposprayComponent, GetVerbsEvent<AlternativeVerb>>(AddToggleModeVerb);
SubscribeLocalEvent<HyposprayComponent, HyposprayDrawDoAfterEvent>(OnDrawDoAfter);
} }
#region Ref events #region Ref events
@@ -63,6 +66,20 @@ public sealed class HypospraySystem : EntitySystem
TryDoInject(entity, args.HitEntities[0], args.User); 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 #endregion
#region Draw/Inject #region Draw/Inject
@@ -73,7 +90,7 @@ public sealed class HypospraySystem : EntitySystem
&& !EligibleEntity(target, entity) && !EligibleEntity(target, entity)
&& _solutionContainers.TryGetDrawableSolution(target, out var drawableSolution, out _)) && _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); return TryDoInject(entity, target, user);
@@ -186,17 +203,37 @@ public sealed class HypospraySystem : EntitySystem
return true; 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, if (!_solutionContainers.TryGetSolution(entity.Owner, entity.Comp.SolutionName, out var soln))
out var solution) || solution.AvailableVolume == 0) 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)
{
amount = null;
if (solutionEntity.Comp.Solution.AvailableVolume == 0)
{ {
return false; return false;
} }
// Get transfer amount. May be smaller than _transferAmount if not enough room, also make sure there's room in the injector // 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, var realTransferAmount = FixedPoint2.Min(entity.Comp.TransferAmount, targetSolution.Comp.Solution.Volume,
solution.AvailableVolume); solutionEntity.Comp.Solution.AvailableVolume);
if (realTransferAmount <= 0) if (realTransferAmount <= 0)
{ {
@@ -207,7 +244,19 @@ public sealed class HypospraySystem : EntitySystem
return false; 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)) if (!_solutionContainers.TryAddSolution(soln.Value, removedSolution))
{ {
@@ -275,3 +324,6 @@ public sealed class HypospraySystem : EntitySystem
#endregion #endregion
} }
[Serializable, NetSerializable]
public sealed partial class HyposprayDrawDoAfterEvent : SimpleDoAfterEvent {}

View File

@@ -36,7 +36,7 @@ public sealed class ScoopableSolutionSystem : EntitySystem
!_solution.TryGetRefillableSolution(beaker, out var target, out _)) !_solution.TryGetRefillableSolution(beaker, out var target, out _))
return false; 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) if (scooped == 0)
return false; return false;

View File

@@ -1,19 +1,19 @@
using System.Diagnostics.CodeAnalysis;
using Content.Shared.Administration.Logs; using Content.Shared.Administration.Logs;
using Content.Shared.Chemistry;
using Content.Shared.Chemistry.Components; using Content.Shared.Chemistry.Components;
using Content.Shared.Database; using Content.Shared.Database;
using Content.Shared.DoAfter;
using Content.Shared.FixedPoint; using Content.Shared.FixedPoint;
using Content.Shared.Interaction; using Content.Shared.Interaction;
using Content.Shared.Popups; using Content.Shared.Popups;
using Content.Shared.Verbs; using Content.Shared.Verbs;
using Robust.Shared.Network; using Robust.Shared.Serialization;
using Robust.Shared.Player;
namespace Content.Shared.Chemistry.EntitySystems; namespace Content.Shared.Chemistry.EntitySystems;
/// <summary> /// <summary>
/// Allows an entity to transfer solutions with a customizable amount per click. /// Allows an entity to transfer solutions with a customizable amount -per click-.
/// Also provides <see cref="Transfer"/> API for other systems. /// Also provides <see cref="Transfer"/>, <see cref="RefillTransfer"/> and <see cref="DrainTransfer"/> API for other systems.
/// </summary> /// </summary>
public sealed class SolutionTransferSystem : EntitySystem public sealed class SolutionTransferSystem : EntitySystem
{ {
@@ -21,6 +21,10 @@ public sealed class SolutionTransferSystem : EntitySystem
[Dependency] private readonly SharedPopupSystem _popup = default!; [Dependency] private readonly SharedPopupSystem _popup = default!;
[Dependency] private readonly SharedSolutionContainerSystem _solution = default!; [Dependency] private readonly SharedSolutionContainerSystem _solution = default!;
[Dependency] private readonly SharedUserInterfaceSystem _ui = default!; [Dependency] private readonly SharedUserInterfaceSystem _ui = default!;
[Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
private EntityQuery<RefillableSolutionComponent> _refillableQuery;
private EntityQuery<DrainableSolutionComponent> _drainableQuery;
/// <summary> /// <summary>
/// Default transfer amounts for the set-transfer verb. /// Default transfer amounts for the set-transfer verb.
@@ -32,28 +36,18 @@ public sealed class SolutionTransferSystem : EntitySystem
base.Initialize(); base.Initialize();
SubscribeLocalEvent<SolutionTransferComponent, GetVerbsEvent<AlternativeVerb>>(AddSetTransferVerbs); SubscribeLocalEvent<SolutionTransferComponent, GetVerbsEvent<AlternativeVerb>>(AddSetTransferVerbs);
SubscribeLocalEvent<SolutionTransferComponent, AfterInteractEvent>(OnAfterInteract);
SubscribeLocalEvent<SolutionTransferComponent, TransferAmountSetValueMessage>(OnTransferAmountSetValueMessage); SubscribeLocalEvent<SolutionTransferComponent, TransferAmountSetValueMessage>(OnTransferAmountSetValueMessage);
} SubscribeLocalEvent<SolutionTransferComponent, AfterInteractEvent>(OnAfterInteract);
SubscribeLocalEvent<SolutionTransferComponent, SolutionDrainTransferDoAfterEvent>(OnSolutionDrainTransferDoAfter);
SubscribeLocalEvent<SolutionTransferComponent, SolutionRefillTransferDoAfterEvent>(OnSolutionFillTransferDoAfter);
private void OnTransferAmountSetValueMessage(Entity<SolutionTransferComponent> ent, ref TransferAmountSetValueMessage message) _refillableQuery = GetEntityQuery<RefillableSolutionComponent>();
{ _drainableQuery = GetEntityQuery<DrainableSolutionComponent>();
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);
} }
private void AddSetTransferVerbs(Entity<SolutionTransferComponent> ent, ref GetVerbsEvent<AlternativeVerb> args) private void AddSetTransferVerbs(Entity<SolutionTransferComponent> ent, ref GetVerbsEvent<AlternativeVerb> args)
{ {
var (uid, comp) = ent; if (!args.CanAccess || !args.CanInteract || !ent.Comp.CanChangeTransferAmount || args.Hands == null)
if (!args.CanAccess || !args.CanInteract || !comp.CanChangeTransferAmount || args.Hands == null)
return; return;
// Custom transfer verb // Custom transfer verb
@@ -66,7 +60,7 @@ public sealed class SolutionTransferSystem : EntitySystem
// TODO: remove server check when bui prediction is a thing // TODO: remove server check when bui prediction is a thing
Act = () => Act = () =>
{ {
_ui.OpenUi(uid, TransferAmountUiKey.Key, @event.User); _ui.OpenUi(ent.Owner, TransferAmountUiKey.Key, @event.User);
}, },
Priority = 1 Priority = 1
}); });
@@ -76,7 +70,7 @@ public sealed class SolutionTransferSystem : EntitySystem
var user = args.User; var user = args.User;
foreach (var amount in DefaultTransferAmounts) foreach (var amount in DefaultTransferAmounts)
{ {
if (amount < comp.MinimumTransferAmount || amount > comp.MaximumTransferAmount) if (amount < ent.Comp.MinimumTransferAmount || amount > ent.Comp.MaximumTransferAmount)
continue; continue;
AlternativeVerb verb = new(); AlternativeVerb verb = new();
@@ -84,11 +78,11 @@ public sealed class SolutionTransferSystem : EntitySystem
verb.Category = VerbCategory.SetTransferAmount; verb.Category = VerbCategory.SetTransferAmount;
verb.Act = () => 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. // 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) private void OnAfterInteract(Entity<SolutionTransferComponent> ent, ref AfterInteractEvent args)
{ {
if (!args.CanReach || args.Target is not {} target) if (!args.CanReach || args.Target is not {} target)
return; 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. // In the case where the target has both Refillable and Drainable, Held --> Target takes priority.
if (comp.CanReceive
&& !HasComp<RefillableSolutionComponent>(target) // target must not be refillable (e.g. Reagent Tanks) if (ent.Comp.CanSend
&& _solution.TryGetDrainableSolution(target, out var targetSoln, out _) // target must be drainable && _drainableQuery.TryComp(ent.Owner, out var heldDrainable)
&& TryComp<RefillableSolutionComponent>(uid, out var refill) && _refillableQuery.TryComp(target, out var targetRefillable)
&& _solution.TryGetRefillableSolution((uid, refill, null), out var ownerSoln, out var ownerRefill)) && 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 var transferAmount = ent.Comp.TransferAmount;
if (refill?.MaxRefill is {} maxRefill) if (targetRefillable.MaxRefill is {} maxRefill)
transferAmount = FixedPoint2.Min(transferAmount, maxRefill); transferAmount = FixedPoint2.Min(transferAmount, maxRefill);
var transferred = Transfer(args.User, target, targetSoln.Value, uid, ownerSoln.Value, transferAmount); var transferData = new SolutionTransferData(args.User, ent.Owner, ownerSoln.Value, target, targetSoln.Value, transferAmount);
args.Handled = true; var transferTime = targetRefillable.RefillTime + heldDrainable.DrainTime;
if (transferred > 0)
if (transferTime > TimeSpan.Zero)
{ {
var toTheBrim = ownerRefill.AvailableVolume == 0; 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 (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))
{
args.Handled = true; //If we reach this point, the interaction counts as handled.
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 transferData = new SolutionTransferData(args.User, target, targetSoln.Value, ent.Owner, ownerSoln.Value, transferAmount);
var transferTime = heldRefillable.RefillTime + targetDrainable.DrainTime;
if (transferTime > TimeSpan.Zero)
{
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 var msg = toTheBrim
? "comp-solution-transfer-fill-fully" ? "comp-solution-transfer-fill-fully"
: "comp-solution-transfer-fill-normal"; : "comp-solution-transfer-fill-normal";
_popup.PopupClient(Loc.GetString(msg, ("owner", args.Target), ("amount", transferred), ("target", uid)), uid, args.User); _popup.PopupClient(Loc.GetString(msg, ("owner", data.SourceEntity), ("amount", transferred), ("target", data.TargetEntity)), data.TargetEntity, data.User);
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 _))
{
var transferAmount = comp.TransferAmount;
if (targetRefill?.MaxRefill is {} maxRefill)
transferAmount = FixedPoint2.Min(transferAmount, maxRefill);
var transferred = Transfer(args.User, uid, ownerSoln.Value, target, targetSoln.Value, transferAmount);
args.Handled = true;
if (transferred > 0)
{
var message = Loc.GetString("comp-solution-transfer-transfer-solution", ("amount", transferred), ("target", target));
_popup.PopupClient(message, uid, args.User);
}
}
} }
/// <summary> /// <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> /// </summary>
/// <returns>The actual amount transferred.</returns> /// <returns>The actual amount transferred.</returns>
public FixedPoint2 Transfer(EntityUid user, public FixedPoint2 Transfer(SolutionTransferData data)
EntityUid sourceEntity,
Entity<SolutionComponent> source,
EntityUid targetEntity,
Entity<SolutionComponent> target,
FixedPoint2 amount)
{ {
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 if (!CanTransfer(data))
RaiseLocalEvent(sourceEntity, ref transferAttempt);
if (transferAttempt.CancelReason is {} reason)
{
_popup.PopupClient(reason, sourceEntity, user);
return FixedPoint2.Zero; return FixedPoint2.Zero;
}
var sourceSolution = source.Comp.Solution; var actualAmount = FixedPoint2.Min(data.Amount, FixedPoint2.Min(sourceSolution.Volume, targetSolution.AvailableVolume));
if (sourceSolution.Volume == 0)
{
_popup.PopupClient(Loc.GetString("comp-solution-transfer-is-empty", ("target", sourceEntity)), sourceEntity, user);
return FixedPoint2.Zero;
}
// Check if the target is cancelling the transfer var solution = _solution.SplitSolution(data.Source, actualAmount);
RaiseLocalEvent(targetEntity, ref transferAttempt); _solution.AddSolution(data.Target, solution);
if (transferAttempt.CancelReason is {} targetReason)
{
_popup.PopupClient(targetReason, targetEntity, user);
return FixedPoint2.Zero;
}
var targetSolution = target.Comp.Solution; var ev = new SolutionTransferredEvent(data.SourceEntity, data.TargetEntity, data.User, actualAmount);
if (targetSolution.AvailableVolume == 0) RaiseLocalEvent(data.TargetEntity, ref ev);
{
_popup.PopupClient(Loc.GetString("comp-solution-transfer-is-full", ("target", targetEntity)), targetEntity, user);
return FixedPoint2.Zero;
}
var actualAmount = FixedPoint2.Min(amount, FixedPoint2.Min(sourceSolution.Volume, targetSolution.AvailableVolume)); _adminLogger.Add(LogType.Action,
LogImpact.Medium,
var solution = _solution.SplitSolution(source, actualAmount); $"{ToPrettyString(data.User):player} transferred {SharedSolutionContainerSystem.ToPrettyString(solution)} to {ToPrettyString(data.TargetEntity):target}, which now contains {SharedSolutionContainerSystem.ToPrettyString(targetSolution)}");
_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)}");
return actualAmount; 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(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;
}
}
/// <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> /// <summary>
@@ -234,3 +412,35 @@ public record struct SolutionTransferAttemptEvent(EntityUid From, EntityUid To,
/// </summary> /// </summary>
[ByRefEvent] [ByRefEvent]
public record struct SolutionTransferredEvent(EntityUid From, EntityUid To, EntityUid User, FixedPoint2 Amount); 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;
}

View 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<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<DrainableSolutionComponent, DragDropDraggedEvent>(OnDrainableDragged);
SubscribeLocalEvent<RefillableSolutionComponent, DrainedTargetEvent>(OnDrainedToRefillableDragged);
SubscribeLocalEvent<DumpableSolutionComponent, DrainedTargetEvent>(OnDrainedToDumpableDragged); SubscribeLocalEvent<DumpableSolutionComponent, DrainedTargetEvent>(OnDrainedToDumpableDragged);
// We use queries for these since CanDropDraggedEvent gets called pretty rapidly // 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) private void OnDrainableCanDragDropped(Entity<DrainableSolutionComponent> ent, ref CanDropDraggedEvent args)
{ {
// Easily drawn-from thing can be dragged onto easily refillable thing. // 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; return;
args.CanDrop = true; args.CanDrop = true;
@@ -121,28 +120,6 @@ public sealed class SolutionDumpingSystem : EntitySystem
_audio.PlayPredicted(AbsorbentComponent.DefaultTransferSound, ent, args.User); _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. // Common checks between dragging handlers.
private bool DragInteractionChecks(EntityUid user, private bool DragInteractionChecks(EntityUid user,
EntityUid sourceContainer, EntityUid sourceContainer,

View File

@@ -661,12 +661,15 @@
maxVol: 10 maxVol: 10
- type: RefillableSolution - type: RefillableSolution
solution: hypospray solution: hypospray
refillTime: 1.25
maxRefill: 5
- type: ExaminableSolution - type: ExaminableSolution
solution: hypospray solution: hypospray
heldOnly: true # Allow examination only when held in hand. heldOnly: true # Allow examination only when held in hand.
exactVolume: true exactVolume: true
- type: Hypospray - type: Hypospray
onlyAffectsMobs: false onlyAffectsMobs: false
drawTime: 1.25
- type: UseDelay - type: UseDelay
delay: 0.5 delay: 0.5
- type: StaticPrice # A new shitcurity meta - type: StaticPrice # A new shitcurity meta