diff --git a/Content.Server/Chemistry/EntitySystems/SolutionContainerSystem.cs b/Content.Server/Chemistry/EntitySystems/SolutionContainerSystem.cs index ea2e9e67d7..53d8f0e8a4 100644 --- a/Content.Server/Chemistry/EntitySystems/SolutionContainerSystem.cs +++ b/Content.Server/Chemistry/EntitySystems/SolutionContainerSystem.cs @@ -18,6 +18,12 @@ namespace Content.Server.Chemistry.EntitySystems; /// public sealed class SolutionChangedEvent : EntityEventArgs { + public readonly Solution Solution; + + public SolutionChangedEvent(Solution solution) + { + Solution = solution; + } } /// @@ -119,6 +125,8 @@ public sealed partial class SolutionContainerSystem : EntitySystem public void UpdateChemicals(EntityUid uid, Solution solutionHolder, bool needsReactionsProcessing = false, ReactionMixerComponent? mixerComponent = null) { + DebugTools.Assert(solutionHolder.Name != null && TryGetSolution(uid, solutionHolder.Name, out var tmp) && tmp == solutionHolder); + // Process reactions if (needsReactionsProcessing && solutionHolder.CanReact) { @@ -126,7 +134,7 @@ public sealed partial class SolutionContainerSystem : EntitySystem } UpdateAppearance(uid, solutionHolder); - RaiseLocalEvent(uid, new SolutionChangedEvent(), true); + RaiseLocalEvent(uid, new SolutionChangedEvent(solutionHolder)); } public void RemoveAllSolution(EntityUid uid, Solution solutionHolder) @@ -224,6 +232,51 @@ public sealed partial class SolutionContainerSystem : EntitySystem return true; } + /// + /// Moves some quantity of a solution from one solution to another. + /// + /// entity holding the source solution + /// entity holding the target solution + /// source solution + /// target solution + /// quantity of solution to move from source to target. If this is a negative number, the source & target roles are reversed. + public bool TryTransferSolution(EntityUid sourceUid, EntityUid targetUid, Solution source, Solution target, FixedPoint2 quantity) + { + if (quantity < 0) + return TryTransferSolution(targetUid, sourceUid, target, source, -quantity); + + quantity = FixedPoint2.Min(quantity, target.AvailableVolume, source.CurrentVolume); + if (quantity == 0) + return false; + + // TODO after #12428 is merged, this should be made into a function that directly transfers reagents. + // currently this is quite inefficient. + target.AddSolution(source.SplitSolution(quantity)); + + UpdateChemicals(sourceUid, source, false); + UpdateChemicals(targetUid, target, true); + return true; + } + + /// + /// Moves some quantity of a solution from one solution to another. + /// + /// entity holding the source solution + /// entity holding the target solution + /// source solution + /// target solution + /// quantity of solution to move from source to target. If this is a negative number, the source & target roles are reversed. + public bool TryTransferSolution(EntityUid sourceUid, EntityUid targetUid, string source, string target, FixedPoint2 quantity) + { + if (!TryGetSolution(sourceUid, source, out var sourceSoln)) + return false; + + if (!TryGetSolution(targetUid, target, out var targetSoln)) + return false; + + return TryTransferSolution(sourceUid, targetUid, sourceSoln, targetSoln, quantity); + } + /// /// Adds a solution to the container, overflowing the rest. /// It will diff --git a/Content.Server/Fluids/Components/AbsorbentComponent.cs b/Content.Server/Fluids/Components/AbsorbentComponent.cs index 193b38a69e..5e02d518e7 100644 --- a/Content.Server/Fluids/Components/AbsorbentComponent.cs +++ b/Content.Server/Fluids/Components/AbsorbentComponent.cs @@ -22,10 +22,11 @@ public sealed class AbsorbentComponent : Component public FixedPoint2 ResidueAmount = FixedPoint2.New(10); // Should be higher than MopLowerLimit /// - /// To leave behind a wet floor, this tool will be unable to take from puddles with a volume less than this amount. + /// To leave behind a wet floor, this tool will be unable to take from puddles with a volume less than this + /// amount. This limit is ignored if the target puddle does not evaporate. /// - [DataField("mopLowerLimit")] - public FixedPoint2 MopLowerLimit = FixedPoint2.New(5); + [DataField("lowerLimit")] + public FixedPoint2 LowerLimit = FixedPoint2.New(5); [DataField("pickupSound")] public SoundSpecifier PickupSound = new SoundPathSpecifier("/Audio/Effects/Fluids/slosh.ogg"); @@ -34,9 +35,9 @@ public sealed class AbsorbentComponent : Component public SoundSpecifier TransferSound = new SoundPathSpecifier("/Audio/Effects/Fluids/watersplash.ogg"); /// - /// Multiplier for the do_after delay for how quickly the mopping happens. + /// Quantity of reagent that this mop can pick up per second. Determines the length of the do-after. /// - [DataField("mopSpeed")] public float MopSpeed = 1; + [DataField("speed")] public float Speed = 10; /// /// How many entities can this tool interact with at once? diff --git a/Content.Server/Fluids/EntitySystems/EvaporationSystem.cs b/Content.Server/Fluids/EntitySystems/EvaporationSystem.cs index 94ddaa2400..ea47240da3 100644 --- a/Content.Server/Fluids/EntitySystems/EvaporationSystem.cs +++ b/Content.Server/Fluids/EntitySystems/EvaporationSystem.cs @@ -1,8 +1,7 @@ -using Content.Server.Chemistry.EntitySystems; +using Content.Server.Chemistry.EntitySystems; using Content.Server.Fluids.Components; using Content.Shared.FixedPoint; using JetBrains.Annotations; -using Robust.Shared.Utility; namespace Content.Server.Fluids.EntitySystems { @@ -14,7 +13,6 @@ namespace Content.Server.Fluids.EntitySystems public override void Update(float frameTime) { base.Update(frameTime); - var queueDelete = new RemQueue(); foreach (var evaporationComponent in EntityManager.EntityQuery()) { var uid = evaporationComponent.Owner; @@ -38,21 +36,9 @@ namespace Content.Server.Fluids.EntitySystems FixedPoint2.Min(FixedPoint2.New(1), solution.CurrentVolume)); // removes 1 unit, or solution current volume, whichever is lower. } - if (solution.CurrentVolume <= 0) - { - EntityManager.QueueDeleteEntity(uid); - } - else if (solution.CurrentVolume <= evaporationComponent.LowerLimit // if puddle is too big or too small to evaporate. - || solution.CurrentVolume >= evaporationComponent.UpperLimit) - { - evaporationComponent.EvaporationToggle = false; // pause evaporation - } - else evaporationComponent.EvaporationToggle = true; // unpause evaporation, e.g. if a puddle previously above evaporation UpperLimit was brought down below evaporation UpperLimit via mopping. - } - - foreach (var evaporationComponent in queueDelete) - { - EntityManager.RemoveComponent(evaporationComponent.Owner, evaporationComponent); + evaporationComponent.EvaporationToggle = + solution.CurrentVolume > evaporationComponent.LowerLimit + && solution.CurrentVolume < evaporationComponent.UpperLimit; } } diff --git a/Content.Server/Fluids/EntitySystems/MoppingSystem.cs b/Content.Server/Fluids/EntitySystems/MoppingSystem.cs index 86b49c9ce6..776a88991c 100644 --- a/Content.Server/Fluids/EntitySystems/MoppingSystem.cs +++ b/Content.Server/Fluids/EntitySystems/MoppingSystem.cs @@ -2,15 +2,15 @@ using Content.Server.Chemistry.Components.SolutionManager; using Content.Server.Chemistry.EntitySystems; using Content.Server.DoAfter; using Content.Server.Fluids.Components; +using Content.Server.Popups; using Content.Shared.Chemistry.Components; using Content.Shared.FixedPoint; using Content.Shared.Interaction; -using Content.Shared.Popups; using Content.Shared.Tag; -using Robust.Shared.Audio; -using Robust.Shared.Player; -using Robust.Shared.Map; using JetBrains.Annotations; +using Robust.Server.GameObjects; +using Robust.Shared.Audio; +using Robust.Shared.Map; namespace Content.Server.Fluids.EntitySystems; @@ -22,8 +22,10 @@ public sealed class MoppingSystem : EntitySystem [Dependency] private readonly TagSystem _tagSystem = default!; [Dependency] private readonly IMapManager _mapManager = default!; [Dependency] private readonly SolutionContainerSystem _solutionSystem = default!; + [Dependency] private readonly PopupSystem _popups = default!; + [Dependency] private readonly AudioSystem _audio = default!; - const string puddlePrototypeId = "PuddleSmear"; // The puddle prototype to use when releasing liquid to the floor, making a new puddle + const string PuddlePrototypeId = "PuddleSmear"; // The puddle prototype to use when releasing liquid to the floor, making a new puddle public override void Initialize() { @@ -35,264 +37,189 @@ public sealed class MoppingSystem : EntitySystem private void OnAfterInteract(EntityUid uid, AbsorbentComponent component, AfterInteractEvent args) { - if (!args.CanReach) // if user cannot reach the target + if (!args.CanReach || args.Handled) + return; + + if (!_solutionSystem.TryGetSolution(args.Used, AbsorbentComponent.SolutionName, out var absorberSoln)) + return; + + if (args.Target is not { Valid: true } target) { + // Add liquid to an empty floor tile + args.Handled = TryCreatePuddle(args.User, args.ClickLocation, component, absorberSoln); return; } - if (args.Handled) // if the event was already handled - { - return; - } - - _solutionSystem.TryGetSolution(args.Used, AbsorbentComponent.SolutionName, out var absorbedSolution); - - if (absorbedSolution is null) - { - return; - } - - var toolAvailableVolume = absorbedSolution.AvailableVolume; - var toolCurrentVolume = absorbedSolution.CurrentVolume; - - // For adding liquid to an empty floor tile - if (args.Target is null) // if a tile is clicked - { - ReleaseToFloor(args.ClickLocation, component, absorbedSolution); - args.Handled = true; - args.User.PopupMessage(args.User, Loc.GetString("mopping-system-release-to-floor")); - return; - } - else if (args.Target is not null) - { - // Handle our do_after logic - HandleDoAfter(args.User, args.Used, args.Target.Value, component, toolCurrentVolume, toolAvailableVolume); - } - - args.Handled = true; - return; + args.Handled = TryPuddleInteract(args.User, uid, target, component, absorberSoln) + || TryEmptyAbsorber(args.User, uid, target, component, absorberSoln) + || TryFillAbsorber(args.User, uid, target, component, absorberSoln); } - private void ReleaseToFloor(EntityCoordinates clickLocation, AbsorbentComponent absorbent, Solution? absorbedSolution) + /// + /// Tries to create a puddle using solutions stored in the absorber entity. + /// + private bool TryCreatePuddle(EntityUid user, EntityCoordinates clickLocation, AbsorbentComponent absorbent, Solution absorberSoln) { - if ((_mapManager.TryGetGrid(clickLocation.GetGridUid(EntityManager), out var mapGrid)) // needs valid grid - && absorbedSolution is not null) // needs a solution to place on the tile - { - TileRef tile = mapGrid.GetTileRef(clickLocation); + if (absorberSoln.CurrentVolume <= 0) + return false; - // Drop some of the absorbed liquid onto the ground - var releaseAmount = FixedPoint2.Min(absorbent.ResidueAmount, absorbedSolution.CurrentVolume); // The release amount specified on the absorbent component, or the amount currently absorbed (whichever is less). - var releasedSolution = _solutionSystem.SplitSolution(absorbent.Owner, absorbedSolution, releaseAmount); // Remove releaseAmount of solution from the absorbent component - _spillableSystem.SpillAt(tile, releasedSolution, puddlePrototypeId); // And spill it onto the tile. - } + if (!_mapManager.TryGetGrid(clickLocation.GetGridUid(EntityManager), out var mapGrid)) + return false; + + var releaseAmount = FixedPoint2.Min(absorbent.ResidueAmount, absorberSoln.CurrentVolume); + var releasedSolution = _solutionSystem.SplitSolution(absorbent.Owner, absorberSoln, releaseAmount); + _spillableSystem.SpillAt(mapGrid.GetTileRef(clickLocation), releasedSolution, PuddlePrototypeId); + _popups.PopupEntity(Loc.GetString("mopping-system-release-to-floor"), user, user); + return true; } - // Handles logic for our different types of valid target. - // Checks for conditions that would prevent a doAfter from starting. - private void HandleDoAfter(EntityUid user, EntityUid used, EntityUid target, AbsorbentComponent component, FixedPoint2 currentVolume, FixedPoint2 availableVolume) + /// + /// Attempt to fill an absorber from some drainable solution. + /// + private bool TryFillAbsorber(EntityUid user, EntityUid used, EntityUid target, AbsorbentComponent component, Solution absorberSoln) { - // Below variables will be set within this function depending on what kind of target was clicked. - // They will be passed to the OnTransferComplete if the doAfter succeeds. + if (absorberSoln.AvailableVolume <= 0 || !TryComp(target, out DrainableSolutionComponent? drainable)) + return false; - EntityUid donor; - EntityUid acceptor; - string donorSolutionName; - string acceptorSolutionName; + if (!_solutionSystem.TryGetDrainableSolution(target, out var drainableSolution)) + return false; - FixedPoint2 transferAmount; + if (drainableSolution.CurrentVolume <= 0) + { + var msg = Loc.GetString("mopping-system-target-container-empty", ("target", target)); + _popups.PopupEntity(msg, user, user); + return true; + } + + // Let's transfer up to to half the tool's available capacity to the tool. + var quantity = FixedPoint2.Max(component.PickupAmount, absorberSoln.AvailableVolume / 2); + quantity = FixedPoint2.Min(quantity, drainableSolution.CurrentVolume); + + DoMopInteraction(user, used, target, component, drainable.Solution, quantity, 1, "mopping-system-drainable-success", component.TransferSound); + return true; + } + + /// + /// Empty an absorber into a refillable solution. + /// + private bool TryEmptyAbsorber(EntityUid user, EntityUid used, EntityUid target, AbsorbentComponent component, Solution absorberSoln) + { + if (absorberSoln.CurrentVolume <= 0 || !TryComp(target, out RefillableSolutionComponent? refillable)) + return false; + + if (!_solutionSystem.TryGetRefillableSolution(target, out var targetSolution)) + return false; - var delay = 1.0f; //default do_after delay in seconds. string msg; - SoundSpecifier sfx; - - // For our purposes, if our target has a PuddleComponent, treat it as a puddle above all else. - if (TryComp(target, out var puddle)) + if (targetSolution.AvailableVolume <= 0) { - // These return conditions will abort BEFORE the do_after is called: - if(!_solutionSystem.TryGetSolution(target, puddle.SolutionName, out var puddleSolution) // puddle Solution is null - || (puddleSolution.TotalVolume <= 0)) // puddle is completely empty - { - return; - } - else if (availableVolume < 0) // mop is completely full - { - msg = "mopping-system-tool-full"; - user.PopupMessage(user, Loc.GetString(msg, ("used", used))); // play message now because we are aborting. - return; - } - // adding to puddles - else if (puddleSolution.TotalVolume < component.MopLowerLimit // if the puddle is too small for the tool to effectively absorb any more solution from it - && currentVolume > 0) // tool needs a solution to dilute the puddle with. - { - // Dilutes the puddle with some solution from the tool - transferAmount = FixedPoint2.Max(component.ResidueAmount, currentVolume); - TryTransfer(used, target, "absorbed", puddle.SolutionName, transferAmount); // Complete the transfer right away, with no doAfter. - - sfx = component.TransferSound; - SoundSystem.Play(sfx.GetSound(), Filter.Pvs(user), used); // Give instant feedback for diluting puddle, so that it's clear that the player is adding to the puddle (as opposed to other behaviours, which have a doAfter). - - msg = "mopping-system-puddle-diluted"; - user.PopupMessage(user, Loc.GetString(msg)); // play message now because we are aborting. - - return; // Do not begin a doAfter. - } - else - { - // Taking from puddles: - - // Determine transferAmount: - transferAmount = FixedPoint2.Min(component.PickupAmount, puddleSolution.TotalVolume, availableVolume); - - // TODO: consider onelining this with the above, using additional args on Min()? - if ((puddleSolution.TotalVolume - transferAmount) < component.MopLowerLimit) // If the transferAmount would bring the puddle below the MopLowerLimit - { - transferAmount = puddleSolution.TotalVolume - component.MopLowerLimit; // Then the transferAmount should bring the puddle down to the MopLowerLimit exactly - } - - donor = target; // the puddle Uid - donorSolutionName = puddle.SolutionName; - - acceptor = used; // the mop/tool Uid - acceptorSolutionName = "absorbed"; // by definition on AbsorbentComponent - - // Set delay/popup/sound if nondefault. Popup and sound will only play on a successful doAfter. - delay = (component.PickupAmount.Float() / 10.0f) * component.MopSpeed; // Delay should scale with PickupAmount, which represents the maximum we can pick up per click. - msg = "mopping-system-puddle-success"; - sfx = component.PickupSound; - - DoMopInteraction(user, used, target, donor, acceptor, component, donorSolutionName, acceptorSolutionName, transferAmount, delay, msg, sfx); - } + msg = Loc.GetString("mopping-system-target-container-full", ("target", target)); + _popups.PopupEntity(msg, user, user); + return true; } - else if ((TryComp(target, out var refillable)) // We can put solution from the tool into the target - && (currentVolume > 0)) // And the tool is wet + + // check if the target container is too small (e.g. syringe) + // TODO this should really be a tag or something, not a capacity check. + if (targetSolution.MaxVolume <= FixedPoint2.New(20)) { - // These return conditions will abort BEFORE the do_after is called: - if (!_solutionSystem.TryGetRefillableSolution(target, out var refillableSolution)) // refillable Solution is null - { - return; - } - else if (refillableSolution.AvailableVolume <= 0) // target container is full (liquid destination) - { - msg = "mopping-system-target-container-full"; - user.PopupMessage(user, Loc.GetString(msg, ("target", target))); // play message now because we are aborting. - return; - } - else if (refillableSolution.MaxVolume <= FixedPoint2.New(20)) // target container is too small (e.g. syringe) - { - msg = "mopping-system-target-container-too-small"; - user.PopupMessage(user, Loc.GetString(msg, ("target", target))); // play message now because we are aborting. - return; - } - else - { - // Determine transferAmount - if (_tagSystem.HasTag(used, "Mop") // if the tool used is a literal mop (and not a sponge, rag, etc.) - && !_tagSystem.HasTag(target, "Wringer")) // and if the target does not have a wringer for properly drying the mop - { - delay = 5.0f; // Should take much longer if you don't have a wringer - - if ((currentVolume / (currentVolume + availableVolume) ) > 0.25) // mop is more than one-quarter full - { - transferAmount = FixedPoint2.Min(refillableSolution.AvailableVolume, currentVolume * 0.6); // squeeze up to 60% of the solution from the mop. - msg = "mopping-system-hand-squeeze-little-wet"; - - if ((currentVolume / (currentVolume + availableVolume) ) > 0.5) // if the mop is more than half full - msg = "mopping-system-hand-squeeze-still-wet"; // overwrites the above - - } - else // mop is less than one-quarter full - { - transferAmount = FixedPoint2.Min(refillableSolution.AvailableVolume, currentVolume); // squeeze remainder of solution from the mop. - msg = "mopping-system-hand-squeeze-dry"; - } - - } - else - { - transferAmount = FixedPoint2.Min(refillableSolution.AvailableVolume, currentVolume); //Transfer all liquid from the tool to the container, but only if it will fit. - msg = "mopping-system-refillable-success"; - delay = 1.0f; - } - - donor = used; // the mop/tool Uid - donorSolutionName = "absorbed"; // by definition on AbsorbentComponent - - acceptor = target; // the refillable container's Uid - acceptorSolutionName = refillable.Solution; - - // Set delay/popup/sound if nondefault. Popup and sound will only play on a successful doAfter. - - sfx = component.TransferSound; - - DoMopInteraction(user, used, target, donor, acceptor, component, donorSolutionName, acceptorSolutionName, transferAmount, delay, msg, sfx); - } + msg = Loc.GetString("mopping-system-target-container-too-small", ("target", target)); + _popups.PopupEntity(msg, user, user); + return true; } - else if (TryComp(target, out var drainable) // We can take solution from the target - && currentVolume <= 0 ) // tool is dry + + float delay; + FixedPoint2 quantity = absorberSoln.CurrentVolume; + + // TODO this really needs cleaning up. Less magic numbers, more data-fields. + + if (_tagSystem.HasTag(used, "Mop") // if the tool used is a literal mop (and not a sponge, rag, etc.) + && !_tagSystem.HasTag(target, "Wringer")) // and if the target does not have a wringer for properly drying the mop { - // These return conditions will abort BEFORE the do_after is called: - if (!_solutionSystem.TryGetDrainableSolution(target, out var drainableSolution)) - { - return; - } - else if (drainableSolution.CurrentVolume <= 0) // target container is empty (liquid source) - { - msg = "mopping-system-target-container-empty"; - user.PopupMessage(user, Loc.GetString(msg, ("target", target))); // play message now because we are returning. - return; - } + delay = 5.0f; // Should take much longer if you don't have a wringer + + var frac = quantity / absorberSoln.MaxVolume; + + // squeeze up to 60% of the solution from the mop if the mop is more than one-quarter full + if (frac > 0.25) + quantity *= 0.6; + + if (frac > 0.5) + msg = "mopping-system-hand-squeeze-still-wet"; + else if (frac > 0.5) + msg = "mopping-system-hand-squeeze-little-wet"; else - { - // Determine transferAmount - transferAmount = FixedPoint2.Min(availableVolume * 0.5, drainableSolution.CurrentVolume); // Let's transfer up to to half the tool's available capacity to the tool. - - donor = target; // the drainable container's Uid - donorSolutionName = drainable.Solution; - - acceptor = used; // the mop/tool Uid - acceptorSolutionName = "absorbed"; // by definition on AbsorbentComponent - - // Set delay/popup/sound if nondefault. Popup and sound will only play on a successful doAfter. - // default delay is fine for this case. - msg = "mopping-system-drainable-success"; - sfx = component.TransferSound; - - DoMopInteraction(user, used, target, donor, acceptor, component, donorSolutionName, acceptorSolutionName, transferAmount, delay, msg, sfx); - } + msg = "mopping-system-hand-squeeze-dry"; } + else + { + msg = "mopping-system-refillable-success"; + delay = 1.0f; + } + + // negative quantity as we are removing solutions from the mop + quantity = -FixedPoint2.Min(targetSolution.AvailableVolume, quantity); + + DoMopInteraction(user, used, target, component, refillable.Solution, quantity, delay, msg, component.TransferSound); + return true; } - private void DoMopInteraction(EntityUid user, EntityUid used, EntityUid target, EntityUid donor, EntityUid acceptor, - AbsorbentComponent component, string donorSolutionName, string acceptorSolutionName, + /// + /// Logic for an absorbing entity interacting with a puddle. + /// + private bool TryPuddleInteract(EntityUid user, EntityUid used, EntityUid target, AbsorbentComponent absorber, Solution absorberSoln) + { + if (!TryComp(target, out PuddleComponent? puddle)) + return false; + + if (!_solutionSystem.TryGetSolution(target, puddle.SolutionName, out var puddleSolution) || puddleSolution.TotalVolume <= 0) + return false; + + FixedPoint2 quantity; + + // Get lower limit for mopping + FixedPoint2 lowerLimit = FixedPoint2.Zero; + if (TryComp(target, out EvaporationComponent? evaporation) + && evaporation.EvaporationToggle + && evaporation.LowerLimit == 0) + { + lowerLimit = absorber.LowerLimit; + } + + // Can our absorber even absorb any liquid? + if (puddleSolution.TotalVolume <= lowerLimit) + { + // Cannot absorb any more liquid. So clearly the user wants to add liquid to the puddle... right? + // This is the old behavior and I CBF fixing this, for the record I don't like this. + + quantity = FixedPoint2.Min(absorber.ResidueAmount, absorberSoln.CurrentVolume); + if (quantity <= 0) + return false; + + // Dilutes the puddle with some solution from the tool + _solutionSystem.TryTransferSolution(used, target, absorberSoln, puddleSolution, quantity); + _audio.PlayPvs(absorber.TransferSound, used); + _popups.PopupEntity(Loc.GetString("mopping-system-puddle-diluted"), user); + return true; + } + + if (absorberSoln.AvailableVolume < 0) + { + _popups.PopupEntity(Loc.GetString("mopping-system-tool-full", ("used", used)), user, user); + return true; + } + + quantity = FixedPoint2.Min(absorber.PickupAmount, puddleSolution.TotalVolume - lowerLimit, absorberSoln.AvailableVolume); + if (quantity <= 0) + return false; + + var delay = absorber.PickupAmount.Float() / absorber.Speed; + DoMopInteraction(user, used, target, absorber, puddle.SolutionName, quantity, delay, "mopping-system-puddle-success", absorber.PickupSound); + return true; + } + + private void DoMopInteraction(EntityUid user, EntityUid used, EntityUid target, AbsorbentComponent component, string targetSolution, FixedPoint2 transferAmount, float delay, string msg, SoundSpecifier sfx) { - var doAfterArgs = new DoAfterEventArgs(user, delay, target: target) - { - BreakOnUserMove = true, - BreakOnStun = true, - BreakOnDamage = true, - MovementThreshold = 0.2f, - BroadcastCancelledEvent = new TransferCancelledEvent() - { - Target = target, - Component = component // (the AbsorbentComponent) - }, - BroadcastFinishedEvent = new TransferCompleteEvent() - { - User = user, - Tool = used, - Target = target, - Donor = donor, - Acceptor = acceptor, - Component = component, - DonorSolutionName = donorSolutionName, - AcceptorSolutionName = acceptorSolutionName, - Message = msg, - Sound = sfx, - TransferAmount = transferAmount - } - }; - // Can't interact with too many entities at once. if (component.MaxInteractingEntities < component.InteractingEntities.Count + 1) return; @@ -301,60 +228,63 @@ public sealed class MoppingSystem : EntitySystem if (!component.InteractingEntities.Add(target)) return; - var result = _doAfterSystem.WaitDoAfter(doAfterArgs); + var doAfterArgs = new DoAfterEventArgs(user, delay, target: target) + { + BreakOnUserMove = true, + BreakOnStun = true, + BreakOnDamage = true, + MovementThreshold = 0.2f, + BroadcastCancelledEvent = new TransferCancelledEvent(target, component), + BroadcastFinishedEvent = new TransferCompleteEvent(used, target, component, targetSolution, msg, sfx, transferAmount) + }; + + _doAfterSystem.DoAfter(doAfterArgs); } private void OnTransferComplete(TransferCompleteEvent ev) { - SoundSystem.Play(ev.Sound.GetSound(), Filter.Pvs(ev.User), ev.Tool); // Play the After SFX - - ev.User.PopupMessage(ev.User, Loc.GetString(ev.Message, ("target", ev.Target), ("used", ev.Tool))); // Play the After popup message - - TryTransfer(ev.Donor, ev.Acceptor, ev.DonorSolutionName, ev.AcceptorSolutionName, ev.TransferAmount); - - ev.Component.InteractingEntities.Remove(ev.Target); // Tell the absorbentComponent that we have stopped interacting with the target. - return; + _audio.PlayPvs(ev.Sound, ev.Tool); + _popups.PopupEntity(ev.Message, ev.Tool); + _solutionSystem.TryTransferSolution(ev.Target, ev.Tool, ev.TargetSolution, AbsorbentComponent.SolutionName, ev.TransferAmount); + ev.Component.InteractingEntities.Remove(ev.Target); } private void OnTransferCancelled(TransferCancelledEvent ev) { - if (!ev.Component.Deleted) - ev.Component.InteractingEntities.Remove(ev.Target); // Tell the absorbentComponent that we have stopped interacting with the target. - - return; - } - - private void TryTransfer(EntityUid donor, EntityUid acceptor, string donorSolutionName, string acceptorSolutionName, FixedPoint2 transferAmount) - { - if (_solutionSystem.TryGetSolution(donor, donorSolutionName, out var donorSolution) // If the donor solution is valid - && _solutionSystem.TryGetSolution(acceptor, acceptorSolutionName, out var acceptorSolution)) // And the acceptor solution is valid - { - var solutionToTransfer = _solutionSystem.SplitSolution(donor, donorSolution, transferAmount); // Split a portion of the donor solution - _solutionSystem.TryAddSolution(acceptor, acceptorSolution, solutionToTransfer); // And add it to the acceptor solution - } + ev.Component.InteractingEntities.Remove(ev.Target); } } - public sealed class TransferCompleteEvent : EntityEventArgs { - public EntityUid User; - public EntityUid Tool; - public EntityUid Target; - public EntityUid Donor; - public EntityUid Acceptor; - public AbsorbentComponent Component { get; init; } = default!; - public string DonorSolutionName = ""; - public string AcceptorSolutionName = ""; - public string Message = ""; - public SoundSpecifier Sound { get; init; } = default!; - public FixedPoint2 TransferAmount; + public readonly EntityUid Tool; + public readonly EntityUid Target; + public readonly AbsorbentComponent Component; + public readonly string TargetSolution; + public readonly string Message; + public readonly SoundSpecifier Sound; + public readonly FixedPoint2 TransferAmount; + public TransferCompleteEvent(EntityUid tool, EntityUid target, AbsorbentComponent component, string targetSolution, string message, SoundSpecifier sound, FixedPoint2 transferAmount) + { + Tool = tool; + Target = target; + Component = component; + TargetSolution = targetSolution; + Message = Loc.GetString(message, ("target", target), ("used", tool)); + Sound = sound; + TransferAmount = transferAmount; + } } public sealed class TransferCancelledEvent : EntityEventArgs { - public EntityUid Target; - public AbsorbentComponent Component { get; init; } = default!; + public readonly EntityUid Target; + public readonly AbsorbentComponent Component; + public TransferCancelledEvent(EntityUid target, AbsorbentComponent component) + { + Target = target; + Component = component; + } } diff --git a/Content.Server/Fluids/EntitySystems/PuddleSystem.cs b/Content.Server/Fluids/EntitySystems/PuddleSystem.cs index ee2690bc93..6502c29043 100644 --- a/Content.Server/Fluids/EntitySystems/PuddleSystem.cs +++ b/Content.Server/Fluids/EntitySystems/PuddleSystem.cs @@ -1,19 +1,14 @@ -using Content.Server.Chemistry.Components.SolutionManager; using Content.Server.Chemistry.EntitySystems; using Content.Server.Fluids.Components; using Content.Shared.Examine; using Content.Shared.FixedPoint; using Content.Shared.Fluids; -using Content.Shared.Slippery; using Content.Shared.StepTrigger.Components; using Content.Shared.StepTrigger.Systems; using JetBrains.Annotations; -using Robust.Server.GameObjects; using Robust.Shared.Audio; using Robust.Shared.Map; using Robust.Shared.Player; -using Robust.Shared.Random; -using Robust.Shared.GameObjects; using Solution = Content.Shared.Chemistry.Components.Solution; namespace Content.Server.Fluids.EntitySystems @@ -24,9 +19,10 @@ namespace Content.Server.Fluids.EntitySystems [Dependency] private readonly SolutionContainerSystem _solutionContainerSystem = default!; [Dependency] private readonly FluidSpreaderSystem _fluidSpreaderSystem = default!; [Dependency] private readonly StepTriggerSystem _stepTrigger = default!; - [Dependency] private readonly SlipperySystem _slipSystem = default!; - [Dependency] private readonly EvaporationSystem _evaporationSystem = default!; + // Using local deletion queue instead of the standard queue so that we can easily "undelete" if a puddle + // loses & then gains reagents in a single tick. + private HashSet _deletionQueue = new(); public override void Initialize() { @@ -39,6 +35,16 @@ namespace Content.Server.Fluids.EntitySystems SubscribeLocalEvent(OnPuddleInit); } + public override void Update(float frameTime) + { + base.Update(frameTime); + foreach (var ent in _deletionQueue) + { + Del(ent); + } + _deletionQueue.Clear(); + } + private void OnPuddleInit(EntityUid uid, PuddleComponent component, ComponentInit args) { var solution = _solutionContainerSystem.EnsureSolution(uid, component.SolutionName); @@ -47,6 +53,16 @@ namespace Content.Server.Fluids.EntitySystems private void OnSolutionUpdate(EntityUid uid, PuddleComponent component, SolutionChangedEvent args) { + if (args.Solution.Name != component.SolutionName) + return; + + if (args.Solution.CurrentVolume <= 0) + { + _deletionQueue.Add(uid); + return; + } + + _deletionQueue.Remove(uid); UpdateSlip(uid, component); UpdateAppearance(uid, component); } @@ -157,13 +173,12 @@ namespace Content.Server.Fluids.EntitySystems } solution.AddSolution(addedSolution); + _solutionContainerSystem.UpdateChemicals(puddleUid, solution, true); if (checkForOverflow && IsOverflowing(puddleUid, puddleComponent)) { _fluidSpreaderSystem.AddOverflowingPuddle(puddleComponent.Owner, puddleComponent); } - RaiseLocalEvent(puddleComponent.Owner, new SolutionChangedEvent(), true); - if (!sound) { return true; diff --git a/Content.Shared/StepTrigger/Systems/StepTriggerSystem.cs b/Content.Shared/StepTrigger/Systems/StepTriggerSystem.cs index ee8a895d3e..89475cba6f 100644 --- a/Content.Shared/StepTrigger/Systems/StepTriggerSystem.cs +++ b/Content.Shared/StepTrigger/Systems/StepTriggerSystem.cs @@ -1,6 +1,7 @@ using Content.Shared.StepTrigger.Components; using Robust.Shared.Collections; using Robust.Shared.GameStates; +using Robust.Shared.Physics; using Robust.Shared.Physics.Components; using Robust.Shared.Physics.Events; @@ -16,6 +17,18 @@ public sealed class StepTriggerSystem : EntitySystem SubscribeLocalEvent(TriggerHandleState); SubscribeLocalEvent(HandleCollide); +#if DEBUG + SubscribeLocalEvent(OnStartup); + } + + private void OnStartup(EntityUid uid, StepTriggerComponent component, ComponentStartup args) + { + if (!component.Active) + return; + + if (!TryComp(uid, out FixturesComponent? fixtures) || fixtures.FixtureCount == 0) + Logger.Warning($"{ToPrettyString(uid)} has an active step trigger without any fixtures."); +#endif } public override void Update(float frameTime) diff --git a/Resources/Prototypes/Entities/Effects/puddle.yml b/Resources/Prototypes/Entities/Effects/puddle.yml index 90185bf4ff..b12774551d 100644 --- a/Resources/Prototypes/Entities/Effects/puddle.yml +++ b/Resources/Prototypes/Entities/Effects/puddle.yml @@ -15,11 +15,11 @@ path: /Audio/Effects/Fluids/splat.ogg recolor: true - type: Clickable - - type: Evaporation - type: Physics - type: Fixtures fixtures: - - shape: + - id: slipFixture + shape: !type:PhysShapeAabb bounds: "-0.4,-0.4,0.4,0.4" mask: @@ -29,6 +29,16 @@ hard: false - type: Appearance - type: PuddleVisualizer + wetFloorEffectThreshold: 0 # non-evaporating puddles don't become sparkles. + +- type: entity + id: EvaporatingPuddle + parent: PuddleBase + abstract: true + components: + - type: Evaporation + - type: PuddleVisualizer + wetFloorEffectThreshold: 5 - type: entity name: gibblets @@ -40,16 +50,12 @@ sprite: Fluids/gibblet.rsi # Placeholder state: gibblet-0 netsync: false - - type: Puddle - wetFloorEffectThreshold: 0 # No wet floor sparkles - type: SolutionContainerManager solutions: puddle: reagents: - ReagentId: Water Quantity: 10 - - type: Appearance - - type: PuddleVisualizer - type: Slippery launchForwardsMultiplier: 2.0 - type: StepTrigger @@ -57,7 +63,7 @@ - type: entity name: puddle id: PuddleSmear - parent: PuddleBase + parent: EvaporatingPuddle description: A puddle of liquid. components: - type: Sprite @@ -75,7 +81,7 @@ - type: entity name: puddle id: PuddleSplatter - parent: PuddleBase + parent: EvaporatingPuddle description: A puddle of liquid. components: - type: Sprite @@ -103,11 +109,6 @@ - type: Puddle overflowVolume: 50 opacityModifier: 8 - - type: Evaporation - evaporateTime: 400 # very slow - - type: Appearance - - type: PuddleVisualizer - wetFloorEffectThreshold: 0 # No wet floor sparkles - type: entity name: vomit @@ -117,11 +118,11 @@ - type: Transform anchored: true - type: Clickable - - type: Evaporation - type: Physics - type: Fixtures fixtures: - - shape: + - id: slipFixture + shape: !type:PhysShapeAabb bounds: "-0.4,-0.4,0.4,0.4" mask: @@ -188,6 +189,7 @@ state: writing-0 netsync: false - type: Puddle + - type: Evaporation evaporateTime: 10 - type: Appearance - type: PuddleVisualizer diff --git a/Resources/Prototypes/Entities/Objects/Consumable/Food/egg.yml b/Resources/Prototypes/Entities/Objects/Consumable/Food/egg.yml index 08abc76662..ebd86d0572 100644 --- a/Resources/Prototypes/Entities/Objects/Consumable/Food/egg.yml +++ b/Resources/Prototypes/Entities/Objects/Consumable/Food/egg.yml @@ -78,17 +78,12 @@ sprite: Fluids/egg_splat.rsi state: egg-0 netsync: false - - type: Puddle - wetFloorEffectThreshold: 0 # No wet floor sparkles - type: SolutionContainerManager solutions: puddle: reagents: - ReagentId: Egg Quantity: 2 - - type: Evaporation - - type: Appearance - - type: PuddleVisualizer - type: entity name: eggshells diff --git a/Resources/Prototypes/Entities/Objects/Consumable/Food/ingredients.yml b/Resources/Prototypes/Entities/Objects/Consumable/Food/ingredients.yml index dc8ffdab9e..9fef48164b 100644 --- a/Resources/Prototypes/Entities/Objects/Consumable/Food/ingredients.yml +++ b/Resources/Prototypes/Entities/Objects/Consumable/Food/ingredients.yml @@ -20,7 +20,6 @@ reagents: - ReagentId: Flour Quantity: 10 - - type: Evaporation - type: Appearance - type: PuddleVisualizer diff --git a/Resources/Prototypes/Entities/Objects/Consumable/Food/produce.yml b/Resources/Prototypes/Entities/Objects/Consumable/Food/produce.yml index ff132217e6..10d6e4868e 100644 --- a/Resources/Prototypes/Entities/Objects/Consumable/Food/produce.yml +++ b/Resources/Prototypes/Entities/Objects/Consumable/Food/produce.yml @@ -407,18 +407,12 @@ sprite: Fluids/tomato_splat.rsi state: puddle-0 netsync: false - - type: Puddle - wetFloorEffectThreshold: 0 # No wet floor sparkles - type: SolutionContainerManager solutions: puddle: reagents: - ReagentId: JuiceTomato Quantity: 10 - - type: Evaporation - lowerLimit: 0 # todo: reimplement stain behaviour, ideally in a way that doesn't use evaporation lowerLimit - - type: Appearance - - type: PuddleVisualizer - type: entity name: eggplant