]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Make a lot more puddle stuff predicted (#38871)
authorPerry Fraser <perryprog@users.noreply.github.com>
Sat, 18 Oct 2025 17:41:56 +0000 (13:41 -0400)
committerGitHub <noreply@github.com>
Sat, 18 Oct 2025 17:41:56 +0000 (17:41 +0000)
* feat: predict evaporation

* refactor: move puddle update logic to shared

* refactor: move more puddle stuff to Shared

Still can't do stuff that creates puddles :(

* refactor: move puddle transfers to shared

* fix: various style fixes + switch to predicted variants

* style: make some puddle stuff private instead of protected

* refactor: move solution dumping to its own system

* docs: clarify Drainable/Dumpable/Refillable docs

Also whacks unneeded VVAccess's.

* fix: audit usages of drainable+refillable

I'm leaving spear and arrow for now... but I don't love it.

* Added an item query I guess

* Review changes

* You can pour out waterguns

* Review changes

* oops

---------

Co-authored-by: Princess Cheeseballs <66055347+Pronana@users.noreply.github.com>
Co-authored-by: SlamBamActionman <slambamactionman@gmail.com>
18 files changed:
Content.Server/Fluids/EntitySystems/PuddleSystem.Evaporation.cs [deleted file]
Content.Server/Fluids/EntitySystems/PuddleSystem.Spillable.cs
Content.Server/Fluids/EntitySystems/PuddleSystem.Transfers.cs [deleted file]
Content.Server/Fluids/EntitySystems/PuddleSystem.cs
Content.Shared/Cabinet/ItemCabinetSystem.cs
Content.Shared/Chemistry/Components/DrainableSolutionComponent.cs
Content.Shared/Chemistry/Components/DumpableSolutionComponent.cs
Content.Shared/Chemistry/Components/RefillableSolutionComponent.cs
Content.Shared/Fluids/Components/EvaporationComponent.cs
Content.Shared/Fluids/EntitySystems/SolutionDumpingSystem.cs [new file with mode: 0644]
Content.Shared/Fluids/SharedPuddleSystem.Evaporation.cs
Content.Shared/Fluids/SharedPuddleSystem.Spillable.cs
Content.Shared/Fluids/SharedPuddleSystem.cs
Content.Shared/Nutrition/EntitySystems/OpenableSystem.cs
Resources/Prototypes/Entities/Objects/Weapons/Guns/Ammunition/Projectiles/shotgun.yml
Resources/Prototypes/Entities/Objects/Weapons/Guns/Basic/watergun.yml
Resources/Prototypes/Entities/Objects/Weapons/Guns/Projectiles/arrows.yml
Resources/Prototypes/Entities/Structures/Specific/Janitor/drain.yml

diff --git a/Content.Server/Fluids/EntitySystems/PuddleSystem.Evaporation.cs b/Content.Server/Fluids/EntitySystems/PuddleSystem.Evaporation.cs
deleted file mode 100644 (file)
index 245ab83..0000000
+++ /dev/null
@@ -1,67 +0,0 @@
-using Content.Shared.Chemistry.Components;
-using Content.Shared.Chemistry.Reagent;
-using Content.Shared.FixedPoint;
-using Content.Shared.Fluids.Components;
-
-namespace Content.Server.Fluids.EntitySystems;
-
-public sealed partial class PuddleSystem
-{
-    private static readonly TimeSpan EvaporationCooldown = TimeSpan.FromSeconds(1);
-
-    private void OnEvaporationMapInit(Entity<EvaporationComponent> entity, ref MapInitEvent args)
-    {
-        entity.Comp.NextTick = _timing.CurTime + EvaporationCooldown;
-    }
-
-    private void UpdateEvaporation(EntityUid uid, Solution solution)
-    {
-        if (HasComp<EvaporationComponent>(uid))
-        {
-            return;
-        }
-
-        if (solution.GetTotalPrototypeQuantity(GetEvaporatingReagents(solution)) > FixedPoint2.Zero)
-        {
-            var evaporation = AddComp<EvaporationComponent>(uid);
-            evaporation.NextTick = _timing.CurTime + EvaporationCooldown;
-            return;
-        }
-
-        RemComp<EvaporationComponent>(uid);
-    }
-
-    private void TickEvaporation()
-    {
-        var query = EntityQueryEnumerator<EvaporationComponent, PuddleComponent>();
-        var xformQuery = GetEntityQuery<TransformComponent>();
-        var curTime = _timing.CurTime;
-        while (query.MoveNext(out var uid, out var evaporation, out var puddle))
-        {
-            if (evaporation.NextTick > curTime)
-                continue;
-
-            evaporation.NextTick += EvaporationCooldown;
-
-            if (!_solutionContainerSystem.ResolveSolution(uid, puddle.SolutionName, ref puddle.Solution, out var puddleSolution))
-                continue;
-
-            // Yes, this means that 50u water + 50u holy water evaporates twice as fast as 100u water.
-            foreach ((string evaporatingReagent, FixedPoint2 evaporatingSpeed) in GetEvaporationSpeeds(puddleSolution))
-            {
-                var reagentTick = evaporation.EvaporationAmount * EvaporationCooldown.TotalSeconds * evaporatingSpeed;
-                puddleSolution.SplitSolutionWithOnly(reagentTick, evaporatingReagent);
-            }
-
-            // Despawn if we're done
-            if (puddleSolution.Volume == FixedPoint2.Zero)
-            {
-                // Spawn a *sparkle*
-                Spawn("PuddleSparkle", xformQuery.GetComponent(uid).Coordinates);
-                QueueDel(uid);
-            }
-
-            _solutionContainerSystem.UpdateChemicals(puddle.Solution.Value);
-        }
-    }
-}
index 880a4395b4cf20a28c4b3f4049738b604b902368..01526d4ee5a6c435f23c24687b56d5c876881f3f 100644 (file)
@@ -1,20 +1,8 @@
-using Content.Server.Chemistry.Containers.EntitySystems;
-using Content.Shared.Chemistry.Components;
 using Content.Shared.Chemistry.EntitySystems;
-using Content.Shared.Chemistry.Reaction;
-using Content.Shared.Chemistry;
-using Content.Shared.Clothing;
-using Content.Shared.CombatMode.Pacification;
 using Content.Shared.Database;
-using Content.Shared.FixedPoint;
 using Content.Shared.Fluids.Components;
-using Content.Shared.IdentityManagement;
-using Content.Shared.Nutrition.EntitySystems;
-using Content.Shared.Popups;
 using Content.Shared.Spillable;
 using Content.Shared.Throwing;
-using Content.Shared.Weapons.Melee.Events;
-using Robust.Shared.Player;
 
 namespace Content.Server.Fluids.EntitySystems;
 
@@ -26,10 +14,8 @@ public sealed partial class PuddleSystem
 
         SubscribeLocalEvent<SpillableComponent, LandEvent>(SpillOnLand);
         // Openable handles the event if it's closed
-        SubscribeLocalEvent<SpillableComponent, MeleeHitEvent>(SplashOnMeleeHit, after: [typeof(OpenableSystem)]);
         SubscribeLocalEvent<SpillableComponent, SolutionContainerOverflowEvent>(OnOverflow);
         SubscribeLocalEvent<SpillableComponent, SpillDoAfterEvent>(OnDoAfter);
-        SubscribeLocalEvent<SpillableComponent, AttemptPacifiedThrowEvent>(OnAttemptPacifiedThrow);
     }
 
     private void OnOverflow(Entity<SpillableComponent> entity, ref SolutionContainerOverflowEvent args)
@@ -41,66 +27,6 @@ public sealed partial class PuddleSystem
         args.Handled = true;
     }
 
-    private void SplashOnMeleeHit(Entity<SpillableComponent> entity, ref MeleeHitEvent args)
-    {
-        if (args.Handled)
-            return;
-
-        // When attacking someone reactive with a spillable entity,
-        // splash a little on them (touch react)
-        // If this also has solution transfer, then assume the transfer amount is how much we want to spill.
-        // Otherwise let's say they want to spill a quarter of its max volume.
-
-        if (!_solutionContainerSystem.TryGetDrainableSolution(entity.Owner, out var soln, out var solution))
-            return;
-
-        var hitCount = args.HitEntities.Count;
-
-        var totalSplit = FixedPoint2.Min(solution.MaxVolume * 0.25, solution.Volume);
-        if (TryComp<SolutionTransferComponent>(entity, out var transfer))
-        {
-            totalSplit = FixedPoint2.Min(transfer.TransferAmount, solution.Volume);
-        }
-
-        // a little lame, but reagent quantity is not very balanced and we don't want people
-        // spilling like 100u of reagent on someone at once!
-        totalSplit = FixedPoint2.Min(totalSplit, entity.Comp.MaxMeleeSpillAmount);
-
-        if (totalSplit == 0)
-            return;
-
-        // Optionally allow further melee handling occur
-        args.Handled = entity.Comp.PreventMelee;
-
-        // First update the hit count so anything that is not reactive wont count towards the total!
-        foreach (var hit in args.HitEntities)
-        {
-            if (!HasComp<ReactiveComponent>(hit))
-                hitCount -= 1;
-        }
-
-        foreach (var hit in args.HitEntities)
-        {
-            if (!HasComp<ReactiveComponent>(hit))
-                continue;
-
-            var splitSolution = _solutionContainerSystem.SplitSolution(soln.Value, totalSplit / hitCount);
-
-            _adminLogger.Add(LogType.MeleeHit, $"{ToPrettyString(args.User)} splashed {SharedSolutionContainerSystem.ToPrettyString(splitSolution):solution} from {ToPrettyString(entity.Owner):entity} onto {ToPrettyString(hit):target}");
-            _reactive.DoEntityReaction(hit, splitSolution, ReactionMethod.Touch);
-
-            _popups.PopupEntity(
-                Loc.GetString("spill-melee-hit-attacker", ("amount", totalSplit / hitCount), ("spillable", entity.Owner),
-                    ("target", Identity.Entity(hit, EntityManager))),
-                hit, args.User);
-
-            _popups.PopupEntity(
-                Loc.GetString("spill-melee-hit-others", ("attacker", args.User), ("spillable", entity.Owner),
-                    ("target", Identity.Entity(hit, EntityManager))),
-                hit, Filter.PvsExcept(args.User), true, PopupType.SmallCaution);
-        }
-    }
-
     private void SpillOnLand(Entity<SpillableComponent> entity, ref LandEvent args)
     {
         if (!_solutionContainerSystem.TryGetSolution(entity.Owner, entity.Comp.SolutionName, out var soln, out var solution))
@@ -114,7 +40,7 @@ public sealed partial class PuddleSystem
 
         if (args.User != null)
         {
-            _adminLogger.Add(LogType.Landed,
+            AdminLogger.Add(LogType.Landed,
                 $"{ToPrettyString(entity.Owner):entity} spilled a solution {SharedSolutionContainerSystem.ToPrettyString(solution):solution} on landing");
         }
 
@@ -122,22 +48,6 @@ public sealed partial class PuddleSystem
         TrySplashSpillAt(entity.Owner, Transform(entity).Coordinates, drainedSolution, out _);
     }
 
-    /// <summary>
-    /// Prevent Pacified entities from throwing items that can spill liquids.
-    /// </summary>
-    private void OnAttemptPacifiedThrow(Entity<SpillableComponent> ent, ref AttemptPacifiedThrowEvent args)
-    {
-        // Don’t care about closed containers.
-        if (Openable.IsClosed(ent))
-            return;
-
-        // Don’t care about empty containers.
-        if (!_solutionContainerSystem.TryGetSolution(ent.Owner, ent.Comp.SolutionName, out _, out var solution) || solution.Volume <= 0)
-            return;
-
-        args.Cancel("pacified-cannot-throw-spill");
-    }
-
     private void OnDoAfter(Entity<SpillableComponent> entity, ref SpillDoAfterEvent args)
     {
         if (args.Handled || args.Cancelled || args.Args.Target == null)
diff --git a/Content.Server/Fluids/EntitySystems/PuddleSystem.Transfers.cs b/Content.Server/Fluids/EntitySystems/PuddleSystem.Transfers.cs
deleted file mode 100644 (file)
index e850f05..0000000
+++ /dev/null
@@ -1,86 +0,0 @@
-using Content.Shared.Chemistry.Components;
-using Content.Shared.DragDrop;
-using Content.Shared.FixedPoint;
-using Content.Shared.Fluids;
-using Content.Shared.Nutrition.EntitySystems;
-
-namespace Content.Server.Fluids.EntitySystems;
-
-public sealed partial class PuddleSystem
-{
-    [Dependency] private readonly OpenableSystem _openable = default!;
-
-    private void InitializeTransfers()
-    {
-        SubscribeLocalEvent<RefillableSolutionComponent, DragDropDraggedEvent>(OnRefillableDragged);
-    }
-
-    private void OnRefillableDragged(Entity<RefillableSolutionComponent> entity, ref DragDropDraggedEvent args)
-    {
-        if (!_actionBlocker.CanComplexInteract(args.User))
-        {
-            _popups.PopupEntity(Loc.GetString("mopping-system-no-hands"), args.User, args.User);
-            return;
-        }
-
-        if (!_solutionContainerSystem.TryGetSolution(entity.Owner, entity.Comp.Solution, out var soln, out var solution) || solution.Volume == FixedPoint2.Zero)
-        {
-            _popups.PopupEntity(Loc.GetString("mopping-system-empty", ("used", entity.Owner)), entity, args.User);
-            return;
-        }
-
-        // Dump reagents into DumpableSolution
-        if (TryComp<DumpableSolutionComponent>(args.Target, out var dump))
-        {
-            if (!_solutionContainerSystem.TryGetDumpableSolution((args.Target, dump, null), out var dumpableSoln, out var dumpableSolution))
-                return;
-
-            if (!_solutionContainerSystem.TryGetDrainableSolution(entity.Owner, out _, out _))
-                return;
-
-            if (_openable.IsClosed(entity))
-                return;
-
-            bool success = true;
-            if (dump.Unlimited)
-            {
-                var split = _solutionContainerSystem.SplitSolution(soln.Value, solution.Volume);
-                dumpableSolution.AddSolution(split, _prototypeManager);
-            }
-            else
-            {
-                var split = _solutionContainerSystem.SplitSolution(soln.Value, dumpableSolution.AvailableVolume);
-                success = _solutionContainerSystem.TryAddSolution(dumpableSoln.Value, split);
-            }
-
-            if (success)
-            {
-                _audio.PlayPvs(AbsorbentComponent.DefaultTransferSound, args.Target);
-            }
-            else
-            {
-                _popups.PopupEntity(Loc.GetString("mopping-system-full", ("used", args.Target)), args.Target, args.User);
-            }
-
-            return;
-        }
-
-        // Take reagents from target
-        if (!TryComp<DrainableSolutionComponent>(args.Target, out var drainable))
-        {
-            if (!_solutionContainerSystem.TryGetDrainableSolution((args.Target, drainable, null), out var drainableSolution, out _))
-                return;
-
-            var split = _solutionContainerSystem.SplitSolution(drainableSolution.Value, solution.AvailableVolume);
-
-            if (_solutionContainerSystem.TryAddSolution(soln.Value, split))
-            {
-                _audio.PlayPvs(AbsorbentComponent.DefaultTransferSound, entity);
-            }
-            else
-            {
-                _popups.PopupEntity(Loc.GetString("mopping-system-full", ("used", entity.Owner)), entity, args.User);
-            }
-        }
-    }
-}
index 63ee75618c8ace683221202b122deba157602670..2f966354eca38ffb744354763fd9de3349f39776 100644 (file)
@@ -1,38 +1,25 @@
-using System.Linq;
-using Content.Server.Administration.Logs;
-using Content.Server.Chemistry.TileReactions;
-using Content.Server.DoAfter;
 using Content.Server.Fluids.Components;
 using Content.Server.Spreader;
-using Content.Shared.ActionBlocker;
 using Content.Shared.Chemistry;
 using Content.Shared.Chemistry.Components;
 using Content.Shared.Chemistry.Components.SolutionManager;
 using Content.Shared.Chemistry.EntitySystems;
 using Content.Shared.Chemistry.Reaction;
-using Content.Shared.Chemistry.Reagent;
 using Content.Shared.Database;
 using Content.Shared.Effects;
 using Content.Shared.FixedPoint;
 using Content.Shared.Fluids;
 using Content.Shared.Fluids.Components;
-using Content.Shared.Friction;
 using Content.Shared.IdentityManagement;
 using Content.Shared.Maps;
-using Content.Shared.Movement.Components;
-using Content.Shared.Movement.Systems;
 using Content.Shared.Popups;
 using Content.Shared.Slippery;
-using Content.Shared.StepTrigger.Components;
-using Content.Shared.StepTrigger.Systems;
-using Robust.Server.Audio;
 using Robust.Shared.Collections;
 using Robust.Shared.Map;
 using Robust.Shared.Map.Components;
 using Robust.Shared.Player;
 using Robust.Shared.Prototypes;
 using Robust.Shared.Random;
-using Robust.Shared.Timing;
 
 namespace Content.Server.Fluids.EntitySystems;
 
@@ -41,28 +28,15 @@ namespace Content.Server.Fluids.EntitySystems;
 /// </summary>
 public sealed partial class PuddleSystem : SharedPuddleSystem
 {
-    [Dependency] private readonly ActionBlockerSystem _actionBlocker = default!;
-    [Dependency] private readonly IAdminLogManager _adminLogger = default!;
-    [Dependency] private readonly IGameTiming _timing = default!;
     [Dependency] private readonly SharedMapSystem _map = default!;
     [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
     [Dependency] private readonly IRobustRandom _random = default!;
-    [Dependency] private readonly AudioSystem _audio = default!;
     [Dependency] private readonly EntityLookupSystem _lookup = default!;
-    [Dependency] private readonly ReactiveSystem _reactive = default!;
     [Dependency] private readonly SharedColorFlashEffectSystem _color = default!;
-    [Dependency] private readonly SharedPopupSystem _popups = default!;
     [Dependency] private readonly SharedSolutionContainerSystem _solutionContainerSystem = default!;
-    [Dependency] private readonly StepTriggerSystem _stepTrigger = default!;
-    [Dependency] private readonly SpeedModifierContactsSystem _speedModContacts = default!;
-    [Dependency] private readonly TileFrictionController _tile = default!;
     [Dependency] private readonly SharedTransformSystem _transform = default!;
     [Dependency] private readonly TurfSystem _turf = 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<EntityUid> _deletionQueue = [];
-
     private EntityQuery<PuddleComponent> _puddleQuery;
 
     /*
@@ -77,16 +51,11 @@ public sealed partial class PuddleSystem : SharedPuddleSystem
 
         _puddleQuery = GetEntityQuery<PuddleComponent>();
 
-        // Shouldn't need re-anchoring.
-        SubscribeLocalEvent<PuddleComponent, AnchorStateChangedEvent>(OnAnchorChanged);
         SubscribeLocalEvent<PuddleComponent, SpreadNeighborsEvent>(OnPuddleSpread);
         SubscribeLocalEvent<PuddleComponent, SlipEvent>(OnPuddleSlip);
-
-        SubscribeLocalEvent<EvaporationComponent, MapInitEvent>(OnEvaporationMapInit);
-
-        InitializeTransfers();
     }
 
+    // TODO: This can be predicted once https://github.com/space-wizards/RobustToolbox/pull/5849 is merged
     private void OnPuddleSpread(Entity<PuddleComponent> entity, ref SpreadNeighborsEvent args)
     {
         // Overflow is the source of the overflowing liquid. This contains the excess fluid above overflow limit (20u)
@@ -273,6 +242,7 @@ public sealed partial class PuddleSystem : SharedPuddleSystem
         }
     }
 
+    // TODO: This can be predicted once https://github.com/space-wizards/RobustToolbox/pull/5849 is merged
     private void OnPuddleSlip(Entity<PuddleComponent> entity, ref SlipEvent args)
     {
         // Reactive entities have a chance to get a touch reaction from slipping on a puddle
@@ -289,168 +259,12 @@ public sealed partial class PuddleSystem : SharedPuddleSystem
                 out var solution))
             return;
 
-        _popups.PopupEntity(Loc.GetString("puddle-component-slipped-touch-reaction", ("puddle", entity.Owner)),
+        Popups.PopupEntity(Loc.GetString("puddle-component-slipped-touch-reaction", ("puddle", entity.Owner)),
             args.Slipped, args.Slipped, PopupType.SmallCaution);
 
         // Take 15% of the puddle solution
         var splitSol = _solutionContainerSystem.SplitSolution(entity.Comp.Solution.Value, solution.Volume * 0.15f);
-        _reactive.DoEntityReaction(args.Slipped, splitSol, ReactionMethod.Touch);
-    }
-
-    /// <inheritdoc/>
-    public override void Update(float frameTime)
-    {
-        base.Update(frameTime);
-        foreach (var ent in _deletionQueue)
-        {
-            Del(ent);
-        }
-
-        _deletionQueue.Clear();
-
-        TickEvaporation();
-    }
-
-    protected override void OnSolutionUpdate(Entity<PuddleComponent> entity, ref SolutionContainerChangedEvent args)
-    {
-        if (args.SolutionId != entity.Comp.SolutionName)
-            return;
-
-        base.OnSolutionUpdate(entity, ref args);
-
-        if (args.Solution.Volume <= 0)
-        {
-            _deletionQueue.Add(entity);
-            return;
-        }
-
-        _deletionQueue.Remove(entity);
-        UpdateSlip((entity, entity.Comp), args.Solution);
-        UpdateSlow(entity, args.Solution);
-        UpdateEvaporation(entity, args.Solution);
-    }
-
-    private void UpdateSlip(Entity<PuddleComponent> entity, Solution solution)
-    {
-        if (!TryComp<StepTriggerComponent>(entity, out var comp))
-            return;
-
-        // Ensure we actually have the component
-        EnsureComp<TileFrictionModifierComponent>(entity);
-
-        EnsureComp<SlipperyComponent>(entity, out var slipComp);
-
-        // This is the base amount of reagent needed before a puddle can be considered slippery. Is defined based on
-        // the sprite threshold for a puddle larger than 5 pixels.
-        var smallPuddleThreshold = FixedPoint2.New(entity.Comp.OverflowVolume.Float() * LowThreshold);
-
-        // Stores how many units of slippery reagents a puddle has
-        var slipperyUnits = FixedPoint2.Zero;
-        // Stores how many units of super slippery reagents a puddle has
-        var superSlipperyUnits = FixedPoint2.Zero;
-
-        // These three values will be averaged later and all start at zero so the calculations work
-        // A cumulative weighted amount of minimum speed to slip values
-        var puddleFriction = FixedPoint2.Zero;
-        // A cumulative weighted amount of minimum speed to slip values
-        var slipStepTrigger = FixedPoint2.Zero;
-        // A cumulative weighted amount of launch multipliers from slippery reagents
-        var launchMult = FixedPoint2.Zero;
-        // A cumulative weighted amount of stun times from slippery reagents
-        var stunTimer = TimeSpan.Zero;
-        // A cumulative weighted amount of knockdown times from slippery reagents
-        var knockdownTimer = TimeSpan.Zero;
-
-        // Check if the puddle is big enough to slip in to avoid doing unnecessary logic
-        if (solution.Volume <= smallPuddleThreshold)
-        {
-            _stepTrigger.SetActive(entity, false, comp);
-            _tile.SetModifier(entity, 1f);
-            slipComp.SlipData.SlipFriction = 1f;
-            slipComp.AffectsSliding = false;
-            Dirty(entity, slipComp);
-            return;
-        }
-
-        slipComp.AffectsSliding = true;
-
-        foreach (var (reagent, quantity) in solution.Contents)
-        {
-            var reagentProto = _prototypeManager.Index<ReagentPrototype>(reagent.Prototype);
-
-            // Calculate the minimum speed needed to slip in the puddle. Average the overall slip thresholds for all reagents
-            var deltaSlipTrigger = reagentProto.SlipData?.RequiredSlipSpeed ?? entity.Comp.DefaultSlippery;
-            slipStepTrigger += quantity * deltaSlipTrigger;
-
-            // Aggregate Friction based on quantity
-            puddleFriction += reagentProto.Friction * quantity;
-
-            if (reagentProto.SlipData == null)
-                continue;
-
-            slipperyUnits += quantity;
-            // Aggregate launch speed based on quantity
-            launchMult += reagentProto.SlipData.LaunchForwardsMultiplier * quantity;
-            // Aggregate stun times based on quantity
-            stunTimer += reagentProto.SlipData.StunTime * (float)quantity;
-            knockdownTimer += reagentProto.SlipData.KnockdownTime * (float)quantity;
-
-            if (reagentProto.SlipData.SuperSlippery)
-                superSlipperyUnits += quantity;
-        }
-
-        // Turn on the step trigger if it's slippery
-        _stepTrigger.SetActive(entity, slipperyUnits > smallPuddleThreshold, comp);
-
-        // This is based of the total volume and not just the slippery volume because there is a default
-        // slippery for all reagents even if they aren't technically slippery.
-        slipComp.SlipData.RequiredSlipSpeed = (float)(slipStepTrigger / solution.Volume);
-        _stepTrigger.SetRequiredTriggerSpeed(entity, slipComp.SlipData.RequiredSlipSpeed);
-
-        // Divide these both by only total amount of slippery reagents.
-        // A puddle with 10 units of lube vs a puddle with 10 of lube and 20 catchup should stun and launch forward the same amount.
-        if (slipperyUnits > 0)
-        {
-            slipComp.SlipData.LaunchForwardsMultiplier = (float)(launchMult/slipperyUnits);
-            slipComp.SlipData.StunTime = (stunTimer/(float)slipperyUnits);
-            slipComp.SlipData.KnockdownTime = (knockdownTimer/(float)slipperyUnits);
-        }
-
-        // Only make it super slippery if there is enough super slippery units for its own puddle
-        slipComp.SlipData.SuperSlippery = superSlipperyUnits >= smallPuddleThreshold;
-
-        // Lower tile friction based on how slippery it is, lets items slide across a puddle of lube
-        slipComp.SlipData.SlipFriction = (float)(puddleFriction / solution.Volume);
-        _tile.SetModifier(entity, slipComp.SlipData.SlipFriction);
-
-        Dirty(entity, slipComp);
-    }
-
-    private void UpdateSlow(EntityUid uid, Solution solution)
-    {
-        var maxViscosity = 0f;
-        foreach (var (reagent, _) in solution.Contents)
-        {
-            var reagentProto = _prototypeManager.Index<ReagentPrototype>(reagent.Prototype);
-            maxViscosity = Math.Max(maxViscosity, reagentProto.Viscosity);
-        }
-
-        if (maxViscosity > 0)
-        {
-            var comp = EnsureComp<SpeedModifierContactsComponent>(uid);
-            var speed = 1 - maxViscosity;
-            _speedModContacts.ChangeSpeedModifiers(uid, speed, comp);
-        }
-        else
-        {
-            RemComp<SpeedModifierContactsComponent>(uid);
-        }
-    }
-
-    private void OnAnchorChanged(Entity<PuddleComponent> entity, ref AnchorStateChangedEvent args)
-    {
-        if (!args.Anchored)
-            QueueDel(entity);
+        Reactive.DoEntityReaction(args.Slipped, splitSol, ReactionMethod.Touch);
     }
 
     /// <summary>
@@ -507,7 +321,7 @@ public sealed partial class PuddleSystem : SharedPuddleSystem
             return true;
         }
 
-        _audio.PlayPvs(puddleComponent.SpillSound, puddleUid);
+        Audio.PlayPvs(puddleComponent.SpillSound, puddleUid);
         return true;
     }
 
@@ -553,6 +367,7 @@ public sealed partial class PuddleSystem : SharedPuddleSystem
 
     #region Spill
 
+    // TODO: This can be predicted once https://github.com/space-wizards/RobustToolbox/pull/5849 is merged
     /// <inheritdoc/>
     public override bool TrySplashSpillAt(EntityUid uid,
         EntityCoordinates coordinates,
@@ -582,13 +397,13 @@ public sealed partial class PuddleSystem : SharedPuddleSystem
 
             if (user != null)
             {
-                _adminLogger.Add(LogType.Landed,
+                AdminLogger.Add(LogType.Landed,
                     $"{ToPrettyString(user.Value):user} threw {ToPrettyString(uid):entity} which splashed a solution {SharedSolutionContainerSystem.ToPrettyString(solution):solution} onto {ToPrettyString(owner):target}");
             }
 
             targets.Add(owner);
-            _reactive.DoEntityReaction(owner, splitSolution, ReactionMethod.Touch);
-            _popups.PopupEntity(
+            Reactive.DoEntityReaction(owner, splitSolution, ReactionMethod.Touch);
+            Popups.PopupEntity(
                 Loc.GetString("spill-land-spilled-on-other", ("spillable", uid),
                     ("target", Identity.Entity(owner, EntityManager))), owner, PopupType.SmallCaution);
         }
index 749065ac4761c9f326c941c833a950ffd4af0a77..ea500aaeaaacc21d2cc77d5353cafa0cefd89b11 100644 (file)
@@ -37,7 +37,7 @@ public sealed class ItemCabinetSystem : EntitySystem
     private void OnMapInit(Entity<ItemCabinetComponent> ent, ref MapInitEvent args)
     {
         // update at mapinit to avoid copy pasting locked: true and locked: false for each closed/open prototype
-        SetSlotLock(ent, !_openable.IsOpen(ent));
+        SetSlotLock(ent, _openable.IsClosed(ent, null));
     }
 
     private void UpdateAppearance(Entity<ItemCabinetComponent> ent)
index 0a6ace0943bd7d823c672fa3cda35315bae94554..11768ca76302184d0fcba4865073045a1f18efe2 100644 (file)
@@ -3,8 +3,9 @@ using Robust.Shared.GameStates;
 namespace Content.Shared.Chemistry.Components;
 
 /// <summary>
-///     Denotes the solution that can be easily removed through any reagent container.
-///     Think pouring this or draining from a water tank.
+/// Denotes a specific solution contained within this entity that can can be
+/// easily "drained". This means things with taps/spigots, or easily poured
+/// items.
 /// </summary>
 [RegisterComponent, NetworkedComponent]
 public sealed partial class DrainableSolutionComponent : Component
@@ -12,6 +13,6 @@ public sealed partial class DrainableSolutionComponent : Component
     /// <summary>
     /// Solution name that can be drained.
     /// </summary>
-    [DataField, ViewVariables(VVAccess.ReadWrite)]
+    [DataField]
     public string Solution = "default";
 }
index 43fbe137b66cf547e9ee149d9f5f410ca166a24f..fadf0358c2c78215a1e4f68d17bfc04832f4baa1 100644 (file)
@@ -3,8 +3,9 @@ using Robust.Shared.GameStates;
 namespace Content.Shared.Chemistry.Components;
 
 /// <summary>
-///     Denotes the solution that can be easily dumped into (completely removed from the dumping container into this one)
-///     Think pouring a container fully into this.
+/// 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.
 /// </summary>
 [RegisterComponent, NetworkedComponent]
 public sealed partial class DumpableSolutionComponent : Component
@@ -12,12 +13,13 @@ public sealed partial class DumpableSolutionComponent : Component
     /// <summary>
     /// Solution name that can be dumped into.
     /// </summary>
-    [DataField, ViewVariables(VVAccess.ReadWrite)]
+    [DataField]
     public string Solution = "default";
 
     /// <summary>
     /// Whether the solution can be dumped into infinitely.
     /// </summary>
-    [DataField, ViewVariables(VVAccess.ReadWrite)]
+    /// <remarks>Note that this is what makes the ChemMaster's buffer a stasis buffer as well!</remarks>
+    [DataField]
     public bool Unlimited = false;
 }
index 245b7398a7e090f91ec2fd032f7ae6e9551adbf6..e42bb68e619bf99409294d49a41d0104684c9706 100644 (file)
@@ -4,9 +4,10 @@ using Robust.Shared.GameStates;
 namespace Content.Shared.Chemistry.Components;
 
 /// <summary>
-///     Reagents that can be added easily. For example like
-///     pouring something into another beaker, glass, or into the gas
-///     tank of a car.
+/// 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.
 /// </summary>
 [RegisterComponent, NetworkedComponent]
 public sealed partial class RefillableSolutionComponent : Component
@@ -14,12 +15,12 @@ public sealed partial class RefillableSolutionComponent : Component
     /// <summary>
     /// Solution name that can added to easily.
     /// </summary>
-    [DataField, ViewVariables(VVAccess.ReadWrite)]
+    [DataField]
     public string Solution = "default";
 
     /// <summary>
     /// The maximum amount that can be transferred to the solution at once
     /// </summary>
-    [DataField, ViewVariables(VVAccess.ReadWrite)]
+    [DataField]
     public FixedPoint2? MaxRefill = null;
 }
index 88cea52945ee938680f5118f517fd00184a0ad22..b3b3ac12d4610da452fba595aa87ae31e2c839ae 100644 (file)
@@ -8,14 +8,15 @@ namespace Content.Shared.Fluids.Components;
 /// <summary>
 /// Added to puddles that contain water so it may evaporate over time.
 /// </summary>
-[NetworkedComponent, AutoGenerateComponentPause]
+[NetworkedComponent, AutoGenerateComponentPause, AutoGenerateComponentState]
 [RegisterComponent, Access(typeof(SharedPuddleSystem))]
 public sealed partial class EvaporationComponent : Component
 {
     /// <summary>
     /// The next time we remove the EvaporationSystem reagent amount from this entity.
     /// </summary>
-    [AutoPausedField, DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
+    [AutoNetworkedField, AutoPausedField]
+    [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
     public TimeSpan NextTick;
 
     /// <summary>
diff --git a/Content.Shared/Fluids/EntitySystems/SolutionDumpingSystem.cs b/Content.Shared/Fluids/EntitySystems/SolutionDumpingSystem.cs
new file mode 100644 (file)
index 0000000..87873d5
--- /dev/null
@@ -0,0 +1,193 @@
+using System.Diagnostics.CodeAnalysis;
+using Content.Shared.ActionBlocker;
+using Content.Shared.Chemistry.Components;
+using Content.Shared.Chemistry.EntitySystems;
+using Content.Shared.DragDrop;
+using Content.Shared.FixedPoint;
+using Content.Shared.Item;
+using Content.Shared.Nutrition.EntitySystems;
+using Content.Shared.Popups;
+using Robust.Shared.Audio.Systems;
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared.Fluids.EntitySystems;
+
+/// <summary>
+/// Handles drag and drop of various solutions.
+/// </summary>
+/// <remarks>
+/// The thing dragged always "gives" its reagents away for consistent UX.
+/// </remarks>
+/// <seealso cref="DumpableSolutionComponent" />
+/// <seealso cref="DrainableSolutionComponent" />
+/// <seealso cref="RefillableSolutionComponent" />
+public sealed class SolutionDumpingSystem : EntitySystem
+{
+    [Dependency] private readonly IPrototypeManager _protoMan = default!;
+    [Dependency] private readonly ActionBlockerSystem _actionBlocker = default!;
+    [Dependency] private readonly OpenableSystem _openable = default!;
+    [Dependency] private readonly SharedAudioSystem _audio = default!;
+    [Dependency] private readonly SharedPopupSystem _popup = default!;
+    [Dependency] private readonly SharedSolutionContainerSystem _solContainer = default!;
+
+    private EntityQuery<ItemComponent> _itemQuery;
+    private EntityQuery<RefillableSolutionComponent> _refillableQuery;
+    private EntityQuery<DumpableSolutionComponent> _dumpQuery;
+
+    public override void Initialize()
+    {
+        base.Initialize();
+
+        SubscribeLocalEvent<DrainableSolutionComponent, CanDragEvent>(OnDrainableCanDrag);
+        SubscribeLocalEvent<DrainableSolutionComponent, CanDropDraggedEvent>(OnDrainableCanDragDropped);
+
+        //SubscribeLocalEvent<RefillableSolutionComponent, DragDropDraggedEvent>(OnRefillableDragged); For if you want to refill a container by dragging it into another one. Can't find a use for that currently.
+        SubscribeLocalEvent<DrainableSolutionComponent, DragDropDraggedEvent>(OnDrainableDragged);
+
+        SubscribeLocalEvent<RefillableSolutionComponent, DrainedTargetEvent>(OnDrainedToRefillableDragged);
+        SubscribeLocalEvent<DumpableSolutionComponent, DrainedTargetEvent>(OnDrainedToDumpableDragged);
+
+        // We use queries for these since CanDropDraggedEvent gets called pretty rapidly
+        _itemQuery = GetEntityQuery<ItemComponent>();
+        _refillableQuery = GetEntityQuery<RefillableSolutionComponent>();
+        _dumpQuery = GetEntityQuery<DumpableSolutionComponent>();
+    }
+
+    private void OnDrainableCanDrag(Entity<DrainableSolutionComponent> ent, ref CanDragEvent args)
+    {
+        if (_itemQuery.HasComp(ent))
+            args.Handled = true;
+    }
+
+    private void OnDrainableCanDragDropped(Entity<DrainableSolutionComponent> ent, ref CanDropDraggedEvent args)
+    {
+        // Easily drawn-from thing can be dragged onto easily refillable thing.
+        if (!_refillableQuery.HasComp(args.Target) && !_dumpQuery.HasComp(args.Target))
+            return;
+
+        args.CanDrop = true;
+        args.Handled = true;
+    }
+
+    /// <summary>
+    /// For when you are pouring something out from the container.
+    /// </summary>
+    private void OnDrainableDragged(Entity<DrainableSolutionComponent> sourceContainer, ref DragDropDraggedEvent args)
+    {
+        // Raising an event to be able to drain into various kind of fillable components.
+        var ev = new DrainedTargetEvent(args.User, sourceContainer, sourceContainer.Comp.Solution);
+        RaiseLocalEvent(args.Target, ref ev);
+    }
+
+    // Note: I feel that DumpableSolutionComponent is kind of redundant and only used to support unlimited containers,
+    // and even then that should probably be refactored out (see to-do below).
+    // It might be worth having the distinction if we want to separate "dump all" vs "pour some" functionalities,
+    // but then we probably want to do a proper pass on how RefillableSolutionComponent is handled.
+    private void OnDrainedToDumpableDragged(Entity<DumpableSolutionComponent> ent, ref DrainedTargetEvent args)
+    {
+        if (!_solContainer.TryGetDumpableSolution((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,
+                !ent.Comp.Unlimited))
+            return;
+
+        if (ent.Comp.Unlimited)
+        {
+            // Unlimited means we're dumping into an infinite buffer, so we
+            // have to be careful that we don't trigger any reactions. This
+            // means SolutionContainerSystem.AddSolution can't be used!
+            // TODO: This should be replaced with proper support for unlimited solutions, rather than cheating by bypassing UpdateChemicals using AddSolution. We can already avoid reactions using CanReact = false, this cheat just bypasses solution overflow.
+            targetSol.AddSolution(
+                _solContainer.SplitSolution(sourceEnt.Value, sourceEnt.Value.Comp.Solution.Volume),
+                _protoMan);
+            // Solution.AddSolution doesn't dirty targetSol for us
+            Dirty(targetSolEnt.Value);
+        }
+        else
+        {
+            _solContainer.TryAddSolution(targetSolEnt.Value,
+                _solContainer.SplitSolution(sourceEnt.Value, targetSol.AvailableVolume));
+        }
+
+        _audio.PlayPredicted(AbsorbentComponent.DefaultTransferSound, ent, args.User);
+    }
+
+    private void OnDrainedToRefillableDragged(Entity<RefillableSolutionComponent> ent, ref DrainedTargetEvent args)
+    {
+        if (!_solContainer.TryGetRefillableSolution((ent, ent.Comp),
+                out var targetSolEnt,
+                out var targetSol))
+            return;
+
+        // Check openness, hands, source being empty, and target being full.
+        if (!DragInteractionChecks(args.User,
+                args.Source,
+                ent.Owner,
+                args.SourceSolution,
+                targetSol,
+                out var sourceEnt))
+            return;
+
+        _solContainer.TryAddSolution(targetSolEnt.Value,
+            _solContainer.SplitSolution(sourceEnt.Value, targetSol.AvailableVolume));
+
+        _audio.PlayPredicted(AbsorbentComponent.DefaultTransferSound, ent, args.User);
+    }
+
+    // Common checks between dragging handlers.
+    private bool DragInteractionChecks(EntityUid user,
+        EntityUid sourceContainer,
+        EntityUid targetContainer,
+        string sourceSolutionName,
+        Solution targetSol,
+        [NotNullWhen(true)] out Entity<SolutionComponent>? sourceSolEnt,
+        bool checkAvailableVolume = true)
+    {
+        sourceSolEnt = null;
+        if (!_actionBlocker.CanComplexInteract(user))
+        {
+            _popup.PopupClient(Loc.GetString("mopping-system-no-hands"), user, user);
+            return false;
+        }
+
+        if (!_solContainer.TryGetSolution(sourceContainer, sourceSolutionName, out sourceSolEnt)
+            || sourceSolEnt.Value.Comp.Solution.Volume == FixedPoint2.Zero)
+        {
+            _popup.PopupClient(Loc.GetString("mopping-system-empty", ("used", sourceContainer)),
+                sourceContainer,
+                user);
+            return false;
+        }
+
+        if (checkAvailableVolume && targetSol.AvailableVolume == FixedPoint2.Zero)
+        {
+            _popup.PopupClient(Loc.GetString("mopping-system-full", ("used", targetContainer)), targetContainer, user);
+            return false;
+        }
+
+        // Both things need to be open. If the entity has nothing to close, it will count as "open".
+        return !_openable.IsClosed(sourceContainer, user, predicted: true)
+               && !_openable.IsClosed(targetContainer, user, predicted: true);
+    }
+}
+
+/// <summary>
+/// Raised directed on a target being drained into.
+/// </summary>
+[ByRefEvent]
+public record struct DrainedTargetEvent(EntityUid User, EntityUid Source, string SourceSolution)
+{
+    public readonly EntityUid User = User;
+    public readonly EntityUid Source = Source;
+    public readonly string SourceSolution = SourceSolution;
+    public bool Handled = false;
+}
index 8b937ed1a73dd9c72d9a4929e3e45f141d0aeb91..985456020406fdbcf4703fdc466b5040d3e3da75 100644 (file)
@@ -1,15 +1,87 @@
+using System.Linq;
 using Content.Shared.Chemistry.Components;
-using Content.Shared.Chemistry.Reagent;
 using Content.Shared.FixedPoint;
+using Content.Shared.Fluids.Components;
 
 namespace Content.Shared.Fluids;
 
 public abstract partial class SharedPuddleSystem
 {
+    private static readonly TimeSpan EvaporationCooldown = TimeSpan.FromSeconds(1);
+
+    private void OnEvaporationMapInit(Entity<EvaporationComponent> ent, ref MapInitEvent args)
+    {
+        ent.Comp.NextTick = _timing.CurTime + EvaporationCooldown;
+        Dirty(ent);
+    }
+
+    private void UpdateEvaporation(EntityUid uid, Solution solution)
+    {
+        if (_evaporationQuery.HasComp(uid))
+            return;
+
+        if (solution.GetTotalPrototypeQuantity(GetEvaporatingReagents(solution)) > FixedPoint2.Zero)
+        {
+            var evaporation = AddComp<EvaporationComponent>(uid);
+            evaporation.NextTick = _timing.CurTime + EvaporationCooldown;
+            Dirty<EvaporationComponent>((uid, evaporation));
+            return;
+        }
+
+        RemComp<EvaporationComponent>(uid);
+    }
+
+    private void TickEvaporation()
+    {
+        var query = EntityQueryEnumerator<EvaporationComponent, PuddleComponent>();
+        var curTime = _timing.CurTime;
+        while (query.MoveNext(out var uid, out var evaporation, out var puddle))
+        {
+            if (evaporation.NextTick > curTime)
+                continue;
+
+            // Necessary to keep client and server in sync so they don't drift
+            evaporation.NextTick += EvaporationCooldown;
+            Dirty(uid, evaporation);
+
+            if (!_solutionContainerSystem.ResolveSolution(uid, puddle.SolutionName, ref puddle.Solution, out var puddleSolution))
+                continue;
+
+            // If we have multiple evaporating reagents in one puddle, just take the average evaporation speed and apply
+            // that to all of them.
+            var evaporationSpeeds = GetEvaporationSpeeds(puddleSolution);
+            if (evaporationSpeeds.Count == 0)
+                continue;
+
+            // Can't use .Average because FixedPoint2
+            var evaporationSpeed = evaporationSpeeds.Values.Sum() / evaporationSpeeds.Count;
+            var reagentProportions = evaporationSpeeds.ToDictionary(kv => kv.Key,
+                kv => puddleSolution.GetTotalPrototypeQuantity(kv.Key) / puddleSolution.Volume);
+
+            // Still have to iterate over one-by-one since the full solution could have non-evaporating solutions.
+            foreach (var (reagent, factor) in reagentProportions)
+            {
+                var reagentTick = evaporation.EvaporationAmount * EvaporationCooldown.TotalSeconds * evaporationSpeed * factor;
+                puddleSolution.SplitSolutionWithOnly(reagentTick, reagent);
+            }
+
+            // Despawn if we're done
+            if (puddleSolution.Volume == FixedPoint2.Zero)
+            {
+                // Spawn a *sparkle*
+                SpawnAttachedTo(evaporation.EvaporationEffect, Transform(uid).Coordinates);
+                PredictedQueueDel(uid);
+            }
+
+            _solutionContainerSystem.UpdateChemicals(puddle.Solution.Value);
+        }
+    }
+
+
     public string[] GetEvaporatingReagents(Solution solution)
     {
-        var evaporatingReagents = new List<string>();
-        foreach (ReagentPrototype solProto in solution.GetReagentPrototypes(_prototypeManager).Keys)
+        List<string> evaporatingReagents = [];
+        foreach (var solProto in solution.GetReagentPrototypes(_prototypeManager).Keys)
         {
             if (solProto.EvaporationSpeed > FixedPoint2.Zero)
                 evaporatingReagents.Add(solProto.ID);
@@ -19,8 +91,8 @@ public abstract partial class SharedPuddleSystem
 
     public string[] GetAbsorbentReagents(Solution solution)
     {
-        var absorbentReagents = new List<string>();
-        foreach (ReagentPrototype solProto in solution.GetReagentPrototypes(_prototypeManager).Keys)
+        List<string> absorbentReagents = [];
+        foreach (var solProto in solution.GetReagentPrototypes(_prototypeManager).Keys)
         {
             if (solProto.Absorbent)
                 absorbentReagents.Add(solProto.ID);
@@ -34,13 +106,13 @@ public abstract partial class SharedPuddleSystem
     }
 
     /// <summary>
-    /// Gets the evaporating speed of the reagents within a solution.
-    /// The speed at which a solution evaporates is the sum of the speed of all evaporating reagents in it.
+    /// Gets a mapping of evaporating speed of the reagents within a solution.
+    /// The speed at which a solution evaporates is the average of the speed of all evaporating reagents in it.
     /// </summary>
     public Dictionary<string, FixedPoint2> GetEvaporationSpeeds(Solution solution)
     {
-        var evaporatingSpeeds = new Dictionary<string, FixedPoint2>();
-        foreach (ReagentPrototype solProto in solution.GetReagentPrototypes(_prototypeManager).Keys)
+        Dictionary<string, FixedPoint2> evaporatingSpeeds = [];
+        foreach (var solProto in solution.GetReagentPrototypes(_prototypeManager).Keys)
         {
             if (solProto.EvaporationSpeed > FixedPoint2.Zero)
             {
index 7d65dd242451796ec2470e5c449fa34a7422f544..05062aed2e818ad7e5214b19eca59b34bb67fc9f 100644 (file)
@@ -1,24 +1,34 @@
+using Content.Shared.Chemistry;
 using Content.Shared.Chemistry.Components;
+using Content.Shared.Chemistry.EntitySystems;
+using Content.Shared.Chemistry.Reaction;
+using Content.Shared.CombatMode.Pacification;
 using Content.Shared.Database;
 using Content.Shared.DoAfter;
 using Content.Shared.Examine;
 using Content.Shared.FixedPoint;
 using Content.Shared.Fluids.Components;
+using Content.Shared.IdentityManagement;
 using Content.Shared.Nutrition.EntitySystems;
+using Content.Shared.Popups;
 using Content.Shared.Spillable;
 using Content.Shared.Verbs;
 using Content.Shared.Weapons.Melee;
+using Content.Shared.Weapons.Melee.Events;
+using Robust.Shared.Player;
 
 namespace Content.Shared.Fluids;
 
 public abstract partial class SharedPuddleSystem
 {
-    [Dependency] protected readonly OpenableSystem Openable = default!;
+    private static readonly FixedPoint2 MeleeHitTransferProportion = 0.25;
 
     protected virtual void InitializeSpillable()
     {
         SubscribeLocalEvent<SpillableComponent, ExaminedEvent>(OnExamined);
         SubscribeLocalEvent<SpillableComponent, GetVerbsEvent<Verb>>(AddSpillVerb);
+        SubscribeLocalEvent<SpillableComponent, MeleeHitEvent>(SplashOnMeleeHit, after: [typeof(OpenableSystem)]);
+        SubscribeLocalEvent<SpillableComponent, AttemptPacifiedThrowEvent>(OnAttemptPacifiedThrow);
     }
 
     private void OnExamined(Entity<SpillableComponent> entity, ref ExaminedEvent args)
@@ -37,7 +47,10 @@ public abstract partial class SharedPuddleSystem
         if (!args.CanAccess || !args.CanInteract || args.Hands == null)
             return;
 
-        if (!_solutionContainerSystem.TryGetSolution(args.Target, entity.Comp.SolutionName, out var soln, out var solution))
+        if (!_solutionContainerSystem.TryGetSolution(args.Target,
+                entity.Comp.SolutionName,
+                out var soln,
+                out var solution))
             return;
 
         if (Openable.IsClosed(args.Target))
@@ -74,7 +87,12 @@ public abstract partial class SharedPuddleSystem
             var user = args.User;
             verb.Act = () =>
             {
-                _doAfterSystem.TryStartDoAfter(new DoAfterArgs(EntityManager, user, entity.Comp.SpillDelay ?? 0, new SpillDoAfterEvent(), entity.Owner, target: entity.Owner)
+                _doAfterSystem.TryStartDoAfter(new DoAfterArgs(EntityManager,
+                    user,
+                    entity.Comp.SpillDelay ?? 0,
+                    new SpillDoAfterEvent(),
+                    entity.Owner,
+                    target: entity.Owner)
                 {
                     BreakOnDamage = true,
                     BreakOnMove = true,
@@ -86,4 +104,89 @@ public abstract partial class SharedPuddleSystem
         verb.DoContactInteraction = true;
         args.Verbs.Add(verb);
     }
+
+    private void SplashOnMeleeHit(Entity<SpillableComponent> entity, ref MeleeHitEvent args)
+    {
+        if (args.Handled)
+            return;
+
+        // When attacking someone reactive with a spillable entity,
+        // splash a little on them (touch react)
+        // If this also has solution transfer, then assume the transfer amount is how much we want to spill.
+        // Otherwise let's say they want to spill a quarter of its max volume.
+
+        if (!_solutionContainerSystem.TryGetDrainableSolution(entity.Owner, out var soln, out var solution))
+            return;
+
+        var hitCount = args.HitEntities.Count;
+
+        var totalSplit = FixedPoint2.Min(solution.MaxVolume * MeleeHitTransferProportion, solution.Volume);
+        if (TryComp<SolutionTransferComponent>(entity, out var transfer))
+            totalSplit = FixedPoint2.Min(transfer.TransferAmount, solution.Volume);
+
+        // a little lame, but reagent quantity is not very balanced and we don't want people
+        // spilling like 100u of reagent on someone at once!
+        totalSplit = FixedPoint2.Min(totalSplit, entity.Comp.MaxMeleeSpillAmount);
+
+        if (totalSplit == 0)
+            return;
+
+        // Optionally allow further melee handling occur
+        args.Handled = entity.Comp.PreventMelee;
+
+        // First update the hit count so anything that is not reactive wont count towards the total!
+        foreach (var hit in args.HitEntities)
+        {
+            if (!_reactiveQuery.HasComp(hit))
+                hitCount -= 1;
+        }
+
+        foreach (var hit in args.HitEntities)
+        {
+            if (!_reactiveQuery.HasComp(hit))
+                continue;
+
+            var splitSolution = _solutionContainerSystem.SplitSolution(soln.Value, totalSplit / hitCount);
+
+            AdminLogger.Add(LogType.MeleeHit,
+                $"{ToPrettyString(args.User):actor} "
+                + $"splashed {SharedSolutionContainerSystem.ToPrettyString(splitSolution):solution} "
+                + $"from {ToPrettyString(entity.Owner):entity} onto {ToPrettyString(hit):target}");
+
+            Reactive.DoEntityReaction(hit, splitSolution, ReactionMethod.Touch);
+
+            Popups.PopupClient(Loc.GetString("spill-melee-hit-attacker",
+                    ("amount", totalSplit / hitCount),
+                    ("spillable", entity.Owner),
+                    ("target", Identity.Entity(hit, EntityManager, args.User))),
+                hit,
+                args.User);
+            Popups.PopupEntity(
+                Loc.GetString("spill-melee-hit-others",
+                    ("attacker", Identity.Entity(args.User, EntityManager)),
+                    ("spillable", entity.Owner),
+                    ("target", Identity.Entity(hit, EntityManager))),
+                hit,
+                Filter.PvsExcept(args.User),
+                true,
+                PopupType.SmallCaution);
+        }
+    }
+
+    /// <summary>
+    /// Prevent Pacified entities from throwing items that can spill liquids.
+    /// </summary>
+    private void OnAttemptPacifiedThrow(Entity<SpillableComponent> ent, ref AttemptPacifiedThrowEvent args)
+    {
+        // Don’t care about closed containers.
+        if (Openable.IsClosed(ent))
+            return;
+
+        // Don’t care about empty containers.
+        if (!_solutionContainerSystem.TryGetSolution(ent.Owner, ent.Comp.SolutionName, out _, out var solution)
+            || solution.Volume <= 0)
+            return;
+
+        args.Cancel("pacified-cannot-throw-spill");
+    }
 }
index 2d0fffa37b6b3054984a3fda4975999d9821caf2..a2ea2627969baf79bee2eceedd22d177e6bba181 100644 (file)
@@ -1,26 +1,46 @@
 using System.Linq;
+using Content.Shared.Administration.Logs;
+using Content.Shared.Chemistry;
 using Content.Shared.Chemistry.Components;
 using Content.Shared.Chemistry.EntitySystems;
+using Content.Shared.Chemistry.Reaction;
 using Content.Shared.Chemistry.Reagent;
 using Content.Shared.DoAfter;
-using Content.Shared.DragDrop;
 using Content.Shared.Examine;
 using Content.Shared.FixedPoint;
 using Content.Shared.Fluids.Components;
+using Content.Shared.Friction;
+using Content.Shared.Movement.Components;
 using Content.Shared.Movement.Events;
+using Content.Shared.Movement.Systems;
+using Content.Shared.Nutrition.EntitySystems;
+using Content.Shared.Popups;
+using Content.Shared.Slippery;
 using Content.Shared.StepTrigger.Components;
+using Content.Shared.StepTrigger.Systems;
+using Robust.Shared.Audio.Systems;
 using Robust.Shared.Containers;
 using Robust.Shared.Map;
 using Robust.Shared.Prototypes;
+using Robust.Shared.Timing;
 
 namespace Content.Shared.Fluids;
 
 public abstract partial class SharedPuddleSystem : EntitySystem
 {
+    [Dependency] private readonly IGameTiming _timing = default!;
     [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
+    [Dependency] protected readonly ISharedAdminLogManager AdminLogger = default!;
+    [Dependency] protected readonly OpenableSystem Openable = default!;
+    [Dependency] protected readonly ReactiveSystem Reactive = default!;
     [Dependency] private readonly SharedAppearanceSystem _appearance = default!;
-    [Dependency] private readonly SharedSolutionContainerSystem _solutionContainerSystem = default!;
+    [Dependency] protected readonly SharedAudioSystem Audio = default!;
     [Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!;
+    [Dependency] protected readonly SharedPopupSystem Popups = default!;
+    [Dependency] private readonly SharedSolutionContainerSystem _solutionContainerSystem = default!;
+    [Dependency] private readonly SpeedModifierContactsSystem _speedModContacts = default!;
+    [Dependency] private readonly StepTriggerSystem _stepTrigger = default!;
+    [Dependency] private readonly TileFrictionController _tile = default!;
 
     private string[] _standoutReagents = [];
 
@@ -31,25 +51,53 @@ public abstract partial class SharedPuddleSystem : EntitySystem
 
     public const float MediumThreshold = 0.6f;
 
+    // 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<EntityUid> _deletionQueue = [];
+
+    private EntityQuery<StepTriggerComponent> _stepTriggerQuery;
+    private EntityQuery<ReactiveComponent> _reactiveQuery;
+    private EntityQuery<EvaporationComponent> _evaporationQuery;
+
     public override void Initialize()
     {
         base.Initialize();
-        SubscribeLocalEvent<RefillableSolutionComponent, CanDragEvent>(OnRefillableCanDrag);
-        SubscribeLocalEvent<DumpableSolutionComponent, CanDropTargetEvent>(OnDumpCanDropTarget);
-        SubscribeLocalEvent<DrainableSolutionComponent, CanDropTargetEvent>(OnDrainCanDropTarget);
-        SubscribeLocalEvent<RefillableSolutionComponent, CanDropDraggedEvent>(OnRefillableCanDropDragged);
-
+        // Shouldn't need re-anchoring.
+        SubscribeLocalEvent<PuddleComponent, AnchorStateChangedEvent>(OnAnchorChanged);
         SubscribeLocalEvent<PuddleComponent, SolutionContainerChangedEvent>(OnSolutionUpdate);
         SubscribeLocalEvent<PuddleComponent, GetFootstepSoundEvent>(OnGetFootstepSound);
         SubscribeLocalEvent<PuddleComponent, ExaminedEvent>(HandlePuddleExamined);
         SubscribeLocalEvent<PuddleComponent, EntRemovedFromContainerMessage>(OnEntRemoved);
 
+        SubscribeLocalEvent<EvaporationComponent, MapInitEvent>(OnEvaporationMapInit);
+
         SubscribeLocalEvent<PrototypesReloadedEventArgs>(OnPrototypesReloaded);
 
+        _stepTriggerQuery = GetEntityQuery<StepTriggerComponent>();
+        _reactiveQuery = GetEntityQuery<ReactiveComponent>();
+        _evaporationQuery = GetEntityQuery<EvaporationComponent>();
+
         CacheStandsout();
         InitializeSpillable();
     }
 
+    public override void Update(float frameTime)
+    {
+        base.Update(frameTime);
+
+        foreach (var ent in _deletionQueue)
+        {
+            // It's possible to have items in the queue that are already being deleted but threw a
+            // SolutionContainerChangedEvent as a part of their shutdown, like during a round restart.
+            if (!TerminatingOrDeleted(ent))
+                PredictedDel(ent);
+        }
+
+        _deletionQueue.Clear();
+
+        TickEvaporation();
+    }
+
     private void OnPrototypesReloaded(PrototypesReloadedEventArgs ev)
     {
         if (ev.WasModified<ReagentPrototype>())
@@ -64,44 +112,22 @@ public abstract partial class SharedPuddleSystem : EntitySystem
         _standoutReagents = [.. _prototypeManager.EnumeratePrototypes<ReagentPrototype>().Where(x => x.Standsout).Select(x => x.ID)];
     }
 
-    protected virtual void OnSolutionUpdate(Entity<PuddleComponent> entity, ref SolutionContainerChangedEvent args)
+    private void OnSolutionUpdate(Entity<PuddleComponent> entity, ref SolutionContainerChangedEvent args)
     {
         if (args.SolutionId != entity.Comp.SolutionName)
             return;
 
-        UpdateAppearance((entity, entity.Comp));
-    }
-
-    private void OnRefillableCanDrag(Entity<RefillableSolutionComponent> entity, ref CanDragEvent args)
-    {
-        args.Handled = true;
-    }
-
-    private void OnDumpCanDropTarget(Entity<DumpableSolutionComponent> entity, ref CanDropTargetEvent args)
-    {
-        if (HasComp<DrainableSolutionComponent>(args.Dragged))
-        {
-            args.CanDrop = true;
-            args.Handled = true;
-        }
-    }
-
-    private void OnDrainCanDropTarget(Entity<DrainableSolutionComponent> entity, ref CanDropTargetEvent args)
-    {
-        if (HasComp<RefillableSolutionComponent>(args.Dragged))
+        if (args.Solution.Volume <= 0)
         {
-            args.CanDrop = true;
-            args.Handled = true;
-        }
-    }
-
-    private void OnRefillableCanDropDragged(Entity<RefillableSolutionComponent> entity, ref CanDropDraggedEvent args)
-    {
-        if (!HasComp<DrainableSolutionComponent>(args.Target) && !HasComp<DumpableSolutionComponent>(args.Target))
+            _deletionQueue.Add(entity);
             return;
+        }
 
-        args.CanDrop = true;
-        args.Handled = true;
+        _deletionQueue.Remove(entity);
+        UpdateSlip((entity, entity.Comp), args.Solution);
+        UpdateSlow(entity, args.Solution);
+        UpdateEvaporation(entity, args.Solution);
+        UpdateAppearance((entity, entity.Comp));
     }
 
     private void OnGetFootstepSound(Entity<PuddleComponent> entity, ref GetFootstepSoundEvent args)
@@ -122,12 +148,12 @@ public abstract partial class SharedPuddleSystem : EntitySystem
     {
         using (args.PushGroup(nameof(PuddleComponent)))
         {
-            if (TryComp<StepTriggerComponent>(entity, out var slippery) && slippery.Active)
+            if (_stepTriggerQuery.TryComp(entity, out var slippery) && slippery.Active)
             {
                 args.PushMarkup(Loc.GetString("puddle-component-examine-is-slippery-text"));
             }
 
-            if (HasComp<EvaporationComponent>(entity) &&
+            if (_evaporationQuery.HasComp(entity) &&
                 _solutionContainerSystem.ResolveSolution(entity.Owner, entity.Comp.SolutionName,
                     ref entity.Comp.Solution, out var solution))
             {
@@ -143,6 +169,12 @@ public abstract partial class SharedPuddleSystem : EntitySystem
         }
     }
 
+    private void OnAnchorChanged(Entity<PuddleComponent> entity, ref AnchorStateChangedEvent args)
+    {
+        if (!args.Anchored)
+            PredictedQueueDel(entity.Owner);
+    }
+
     // Workaround for https://github.com/space-wizards/space-station-14/pull/35314
     private void OnEntRemoved(Entity<PuddleComponent> ent, ref EntRemovedFromContainerMessage args)
     {
@@ -191,6 +223,122 @@ public abstract partial class SharedPuddleSystem : EntitySystem
         _appearance.SetData(ent, PuddleVisuals.SolutionColor, color, appearance);
     }
 
+    private void UpdateSlip(Entity<PuddleComponent> entity, Solution solution)
+    {
+        if (!_stepTriggerQuery.TryComp(entity, out var comp))
+            return;
+
+        // Ensure we actually have the component
+        EnsureComp<TileFrictionModifierComponent>(entity);
+        EnsureComp<SlipperyComponent>(entity, out var slipComp);
+
+        // This is the base amount of reagent needed before a puddle can be considered slippery. Is defined based on
+        // the sprite threshold for a puddle larger than 5 pixels.
+        var smallPuddleThreshold = FixedPoint2.New(entity.Comp.OverflowVolume.Float() * LowThreshold);
+
+        // Stores how many units of slippery reagents a puddle has
+        var slipperyUnits = FixedPoint2.Zero;
+        // Stores how many units of super slippery reagents a puddle has
+        var superSlipperyUnits = FixedPoint2.Zero;
+
+        // These three values will be averaged later and all start at zero so the calculations work
+        // A cumulative weighted amount of minimum speed to slip values
+        var puddleFriction = FixedPoint2.Zero;
+        // A cumulative weighted amount of minimum speed to slip values
+        var slipStepTrigger = FixedPoint2.Zero;
+        // A cumulative weighted amount of launch multipliers from slippery reagents
+        var launchMult = FixedPoint2.Zero;
+        // A cumulative weighted amount of stun times from slippery reagents
+        var stunTimer = TimeSpan.Zero;
+        // A cumulative weighted amount of knockdown times from slippery reagents
+        var knockdownTimer = TimeSpan.Zero;
+
+        // Check if the puddle is big enough to slip in to avoid doing unnecessary logic
+        if (solution.Volume <= smallPuddleThreshold)
+        {
+            _stepTrigger.SetActive(entity, false, comp);
+            _tile.SetModifier(entity, 1f);
+            slipComp.SlipData.SlipFriction = 1f;
+            slipComp.AffectsSliding = false;
+            Dirty(entity, slipComp);
+            return;
+        }
+
+        slipComp.AffectsSliding = true;
+
+        foreach (var (reagent, quantity) in solution.Contents)
+        {
+            var reagentProto = _prototypeManager.Index<ReagentPrototype>(reagent.Prototype);
+
+            // Calculate the minimum speed needed to slip in the puddle. Average the overall slip thresholds for all reagents
+            var deltaSlipTrigger = reagentProto.SlipData?.RequiredSlipSpeed ?? entity.Comp.DefaultSlippery;
+            slipStepTrigger += quantity * deltaSlipTrigger;
+
+            // Aggregate Friction based on quantity
+            puddleFriction += reagentProto.Friction * quantity;
+
+            if (reagentProto.SlipData == null)
+                continue;
+
+            slipperyUnits += quantity;
+            // Aggregate launch speed based on quantity
+            launchMult += reagentProto.SlipData.LaunchForwardsMultiplier * quantity;
+            // Aggregate stun times based on quantity
+            stunTimer += reagentProto.SlipData.StunTime * (float)quantity;
+            knockdownTimer += reagentProto.SlipData.KnockdownTime * (float)quantity;
+
+            if (reagentProto.SlipData.SuperSlippery)
+                superSlipperyUnits += quantity;
+        }
+
+        // Turn on the step trigger if it's slippery
+        _stepTrigger.SetActive(entity, slipperyUnits > smallPuddleThreshold, comp);
+
+        // This is based of the total volume and not just the slippery volume because there is a default
+        // slippery for all reagents even if they aren't technically slippery.
+        slipComp.SlipData.RequiredSlipSpeed = (float)(slipStepTrigger / solution.Volume);
+        _stepTrigger.SetRequiredTriggerSpeed(entity, slipComp.SlipData.RequiredSlipSpeed);
+
+        // Divide these both by only total amount of slippery reagents.
+        // A puddle with 10 units of lube vs a puddle with 10 of lube and 20 catchup should stun and launch forward the same amount.
+        if (slipperyUnits > 0)
+        {
+            slipComp.SlipData.LaunchForwardsMultiplier = (float)(launchMult/slipperyUnits);
+            slipComp.SlipData.StunTime = (stunTimer/(float)slipperyUnits);
+            slipComp.SlipData.KnockdownTime = (knockdownTimer/(float)slipperyUnits);
+        }
+
+        // Only make it super slippery if there is enough super slippery units for its own puddle
+        slipComp.SlipData.SuperSlippery = superSlipperyUnits >= smallPuddleThreshold;
+
+        // Lower tile friction based on how slippery it is, lets items slide across a puddle of lube
+        slipComp.SlipData.SlipFriction = (float)(puddleFriction/solution.Volume);
+        _tile.SetModifier(entity, slipComp.SlipData.SlipFriction);
+
+        Dirty(entity, slipComp);
+    }
+
+    private void UpdateSlow(EntityUid uid, Solution solution)
+    {
+        var maxViscosity = 0f;
+        foreach (var (reagent, _) in solution.Contents)
+        {
+            var reagentProto = _prototypeManager.Index<ReagentPrototype>(reagent.Prototype);
+            maxViscosity = Math.Max(maxViscosity, reagentProto.Viscosity);
+        }
+
+        if (maxViscosity > 0)
+        {
+            var comp = EnsureComp<SpeedModifierContactsComponent>(uid);
+            var speed = 1 - maxViscosity;
+            _speedModContacts.ChangeSpeedModifiers(uid, speed, comp);
+        }
+        else
+        {
+            RemComp<SpeedModifierContactsComponent>(uid);
+        }
+    }
+
     public void DoTileReactions(TileRef tileRef, Solution solution)
     {
         for (var i = solution.Contents.Count - 1; i >= 0; i--)
@@ -213,11 +361,11 @@ public abstract partial class SharedPuddleSystem : EntitySystem
     // replicate those, and I am not enough of a wizard to attempt implementing that.
 
     /// <summary>
-    ///     First splashes reagent on reactive entities near the spilling entity, then spills the rest regularly to a
-    ///     puddle. This is intended for 'destructive' spills, like when entities are destroyed or thrown.
+    /// First splashes reagent on reactive entities near the spilling entity, then spills the rest regularly to a
+    /// puddle. This is intended for 'destructive' spills, like when entities are destroyed or thrown.
     /// </summary>
     /// <remarks>
-    /// On the client, this will always set <paramref name="puddleUid"/> to <see cref="EntityUid.Invalid"> and return false.
+    /// On the client, this will always set <paramref name="puddleUid"/> to <see cref="EntityUid.Invalid"/> and return false.
     /// </remarks>
     public abstract bool TrySplashSpillAt(EntityUid uid,
         EntityCoordinates coordinates,
@@ -227,29 +375,19 @@ public abstract partial class SharedPuddleSystem : EntitySystem
         EntityUid? user = null);
 
     /// <summary>
-    ///     Spills solution at the specified coordinates.
+    /// Spills solution at the specified coordinates.
     /// Will add to an existing puddle if present or create a new one if not.
     /// </summary>
     /// <remarks>
-    /// On the client, this will always set <paramref name="puddleUid"/> to <see cref="EntityUid.Invalid"> and return false.
+    /// On the client, this will always set <paramref name="puddleUid"/> to <see cref="EntityUid.Invalid"/> and return false.
     /// </remarks>
     public abstract bool TrySpillAt(EntityCoordinates coordinates, Solution solution, out EntityUid puddleUid, bool sound = true);
 
-    /// <summary>
-    /// <see cref="TrySpillAt(EntityCoordinates, Solution, out EntityUid, bool)"/>
-    /// </summary>
-    /// <remarks>
-    /// On the client, this will always set <paramref name="puddleUid"/> to <see cref="EntityUid.Invalid"> and return false.
-    /// </remarks>
+    /// <inheritdoc cref="TrySpillAt(EntityCoordinates, Solution, out EntityUid, bool)"/>
     public abstract bool TrySpillAt(EntityUid uid, Solution solution, out EntityUid puddleUid, bool sound = true,
         TransformComponent? transformComponent = null);
 
-    /// <summary>
-    /// <see cref="TrySpillAt(EntityCoordinates, Solution, out EntityUid, bool)"/>
-    /// </summary>
-    /// <remarks>
-    /// On the client, this will always set <paramref name="puddleUid"/> to <see cref="EntityUid.Invalid"> and return false.
-    /// </remarks>
+    /// <inheritdoc cref="TrySpillAt(EntityCoordinates, Solution, out EntityUid, bool)"/>
     public abstract bool TrySpillAt(TileRef tileRef, Solution solution, out EntityUid puddleUid, bool sound = true,
         bool tileReact = true);
 
index 04d3c20a3fcce32b33ea02043e711f65480e1c45..1a87dd25e9f80ef5d0ceb8c6169df38de57769b9 100644 (file)
@@ -147,18 +147,6 @@ public sealed partial class OpenableSystem : EntitySystem
             args.Cancelled = true;
     }
 
-    /// <summary>
-    /// Returns true if the entity either does not have OpenableComponent or it is opened.
-    /// Drinks that don't have OpenableComponent are automatically open, so it returns true.
-    /// </summary>
-    public bool IsOpen(EntityUid uid, OpenableComponent? comp = null)
-    {
-        if (!Resolve(uid, ref comp, false))
-            return true;
-
-        return comp.Opened;
-    }
-
     /// <summary>
     /// Returns true if the entity both has OpenableComponent and is not opened.
     /// Drinks that don't have OpenableComponent are automatically open, so it returns false.
index ebea6dde5e084f9b72a1edafd0514a5d9830c5d2..02ccd1a83be74e1350ad082b15082ba964261b22 100644 (file)
     solutions:
       ammo:
         maxVol: 15
-  - type: RefillableSolution
-    solution: ammo
-  - type: DrainableSolution
-    solution: ammo
   - type: SolutionInjectOnProjectileHit
     transferAmount: 15
     solution: ammo
-  - type: InjectableSolution
-    solution: ammo
 
 - type: entity
   id: PelletShotgunFlare
index 65535223d9953a695bc2ce775c5f572f4aa09f61..8222481e92599af5950667041e3be39e79b4fa62 100644 (file)
         type: TransferAmountBoundUserInterface
   - type: DrawableSolution
     solution: chamber
-  - type: RefillableSolution
-    solution: chamber
   - type: DrainableSolution
     solution: chamber
+  - type: RefillableSolution
+    solution: chamber
   - type: ExaminableSolution
     solution: chamber
   - type: StaticPrice
index 3f9a2d5435dd7a0bfef0605da096702ed686d8be..0a577b51dbb3d669519feddc265fc178f8d4d7aa 100644 (file)
@@ -45,7 +45,7 @@
     solutions:
       ammo:
         maxVol: 2
-  - type: RefillableSolution
+  - type: RefillableSolution # This is sus. You can't really just run an arrowhead under a sink faucet.
     solution: ammo
   - type: InjectableSolution
     solution: ammo
index 2a7e96ae620e049d6a60bc3730151e445e768f4d..770d87fd1d9cbf4ed2432049ea27f1995f7504bb 100644 (file)
@@ -38,8 +38,6 @@
       solutions:
         drainBuffer:
           maxVol: 1000
-    - type: DrainableSolution
-      solution: drainBuffer
     - type: Damageable
       damageContainer: StructuralInorganic
       damageModifierSet: StructuralMetallic