diff --git a/Content.Server/Fluids/EntitySystems/PuddleSystem.Evaporation.cs b/Content.Server/Fluids/EntitySystems/PuddleSystem.Evaporation.cs deleted file mode 100644 index 245ab8308f..0000000000 --- a/Content.Server/Fluids/EntitySystems/PuddleSystem.Evaporation.cs +++ /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 entity, ref MapInitEvent args) - { - entity.Comp.NextTick = _timing.CurTime + EvaporationCooldown; - } - - private void UpdateEvaporation(EntityUid uid, Solution solution) - { - if (HasComp(uid)) - { - return; - } - - if (solution.GetTotalPrototypeQuantity(GetEvaporatingReagents(solution)) > FixedPoint2.Zero) - { - var evaporation = AddComp(uid); - evaporation.NextTick = _timing.CurTime + EvaporationCooldown; - return; - } - - RemComp(uid); - } - - private void TickEvaporation() - { - var query = EntityQueryEnumerator(); - var xformQuery = GetEntityQuery(); - 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); - } - } -} diff --git a/Content.Server/Fluids/EntitySystems/PuddleSystem.Spillable.cs b/Content.Server/Fluids/EntitySystems/PuddleSystem.Spillable.cs index 880a4395b4..01526d4ee5 100644 --- a/Content.Server/Fluids/EntitySystems/PuddleSystem.Spillable.cs +++ b/Content.Server/Fluids/EntitySystems/PuddleSystem.Spillable.cs @@ -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(SpillOnLand); // Openable handles the event if it's closed - SubscribeLocalEvent(SplashOnMeleeHit, after: [typeof(OpenableSystem)]); SubscribeLocalEvent(OnOverflow); SubscribeLocalEvent(OnDoAfter); - SubscribeLocalEvent(OnAttemptPacifiedThrow); } private void OnOverflow(Entity entity, ref SolutionContainerOverflowEvent args) @@ -41,66 +27,6 @@ public sealed partial class PuddleSystem args.Handled = true; } - private void SplashOnMeleeHit(Entity 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(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(hit)) - hitCount -= 1; - } - - foreach (var hit in args.HitEntities) - { - if (!HasComp(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 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 _); } - /// - /// Prevent Pacified entities from throwing items that can spill liquids. - /// - private void OnAttemptPacifiedThrow(Entity 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 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 index e850f058a8..0000000000 --- a/Content.Server/Fluids/EntitySystems/PuddleSystem.Transfers.cs +++ /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(OnRefillableDragged); - } - - private void OnRefillableDragged(Entity 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(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(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); - } - } - } -} diff --git a/Content.Server/Fluids/EntitySystems/PuddleSystem.cs b/Content.Server/Fluids/EntitySystems/PuddleSystem.cs index 63ee75618c..2f966354ec 100644 --- a/Content.Server/Fluids/EntitySystems/PuddleSystem.cs +++ b/Content.Server/Fluids/EntitySystems/PuddleSystem.cs @@ -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; /// 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 _deletionQueue = []; - private EntityQuery _puddleQuery; /* @@ -77,16 +51,11 @@ public sealed partial class PuddleSystem : SharedPuddleSystem _puddleQuery = GetEntityQuery(); - // Shouldn't need re-anchoring. - SubscribeLocalEvent(OnAnchorChanged); SubscribeLocalEvent(OnPuddleSpread); SubscribeLocalEvent(OnPuddleSlip); - - SubscribeLocalEvent(OnEvaporationMapInit); - - InitializeTransfers(); } + // TODO: This can be predicted once https://github.com/space-wizards/RobustToolbox/pull/5849 is merged private void OnPuddleSpread(Entity 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 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); - } - - /// - public override void Update(float frameTime) - { - base.Update(frameTime); - foreach (var ent in _deletionQueue) - { - Del(ent); - } - - _deletionQueue.Clear(); - - TickEvaporation(); - } - - protected override void OnSolutionUpdate(Entity 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 entity, Solution solution) - { - if (!TryComp(entity, out var comp)) - return; - - // Ensure we actually have the component - EnsureComp(entity); - - EnsureComp(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(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(reagent.Prototype); - maxViscosity = Math.Max(maxViscosity, reagentProto.Viscosity); - } - - if (maxViscosity > 0) - { - var comp = EnsureComp(uid); - var speed = 1 - maxViscosity; - _speedModContacts.ChangeSpeedModifiers(uid, speed, comp); - } - else - { - RemComp(uid); - } - } - - private void OnAnchorChanged(Entity entity, ref AnchorStateChangedEvent args) - { - if (!args.Anchored) - QueueDel(entity); + Reactive.DoEntityReaction(args.Slipped, splitSol, ReactionMethod.Touch); } /// @@ -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 /// 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); } diff --git a/Content.Shared/Cabinet/ItemCabinetSystem.cs b/Content.Shared/Cabinet/ItemCabinetSystem.cs index 749065ac47..ea500aaeaa 100644 --- a/Content.Shared/Cabinet/ItemCabinetSystem.cs +++ b/Content.Shared/Cabinet/ItemCabinetSystem.cs @@ -37,7 +37,7 @@ public sealed class ItemCabinetSystem : EntitySystem private void OnMapInit(Entity 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 ent) diff --git a/Content.Shared/Chemistry/Components/DrainableSolutionComponent.cs b/Content.Shared/Chemistry/Components/DrainableSolutionComponent.cs index 0a6ace0943..11768ca763 100644 --- a/Content.Shared/Chemistry/Components/DrainableSolutionComponent.cs +++ b/Content.Shared/Chemistry/Components/DrainableSolutionComponent.cs @@ -3,8 +3,9 @@ using Robust.Shared.GameStates; namespace Content.Shared.Chemistry.Components; /// -/// 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. /// [RegisterComponent, NetworkedComponent] public sealed partial class DrainableSolutionComponent : Component @@ -12,6 +13,6 @@ public sealed partial class DrainableSolutionComponent : Component /// /// Solution name that can be drained. /// - [DataField, ViewVariables(VVAccess.ReadWrite)] + [DataField] public string Solution = "default"; } diff --git a/Content.Shared/Chemistry/Components/DumpableSolutionComponent.cs b/Content.Shared/Chemistry/Components/DumpableSolutionComponent.cs index 43fbe137b6..fadf0358c2 100644 --- a/Content.Shared/Chemistry/Components/DumpableSolutionComponent.cs +++ b/Content.Shared/Chemistry/Components/DumpableSolutionComponent.cs @@ -3,8 +3,9 @@ using Robust.Shared.GameStates; namespace Content.Shared.Chemistry.Components; /// -/// 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. /// [RegisterComponent, NetworkedComponent] public sealed partial class DumpableSolutionComponent : Component @@ -12,12 +13,13 @@ public sealed partial class DumpableSolutionComponent : Component /// /// Solution name that can be dumped into. /// - [DataField, ViewVariables(VVAccess.ReadWrite)] + [DataField] public string Solution = "default"; /// /// Whether the solution can be dumped into infinitely. /// - [DataField, ViewVariables(VVAccess.ReadWrite)] + /// Note that this is what makes the ChemMaster's buffer a stasis buffer as well! + [DataField] public bool Unlimited = false; } diff --git a/Content.Shared/Chemistry/Components/RefillableSolutionComponent.cs b/Content.Shared/Chemistry/Components/RefillableSolutionComponent.cs index 245b7398a7..e42bb68e61 100644 --- a/Content.Shared/Chemistry/Components/RefillableSolutionComponent.cs +++ b/Content.Shared/Chemistry/Components/RefillableSolutionComponent.cs @@ -4,9 +4,10 @@ using Robust.Shared.GameStates; namespace Content.Shared.Chemistry.Components; /// -/// 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. /// [RegisterComponent, NetworkedComponent] public sealed partial class RefillableSolutionComponent : Component @@ -14,12 +15,12 @@ public sealed partial class RefillableSolutionComponent : Component /// /// Solution name that can added to easily. /// - [DataField, ViewVariables(VVAccess.ReadWrite)] + [DataField] public string Solution = "default"; /// /// The maximum amount that can be transferred to the solution at once /// - [DataField, ViewVariables(VVAccess.ReadWrite)] + [DataField] public FixedPoint2? MaxRefill = null; } diff --git a/Content.Shared/Fluids/Components/EvaporationComponent.cs b/Content.Shared/Fluids/Components/EvaporationComponent.cs index 88cea52945..b3b3ac12d4 100644 --- a/Content.Shared/Fluids/Components/EvaporationComponent.cs +++ b/Content.Shared/Fluids/Components/EvaporationComponent.cs @@ -8,14 +8,15 @@ namespace Content.Shared.Fluids.Components; /// /// Added to puddles that contain water so it may evaporate over time. /// -[NetworkedComponent, AutoGenerateComponentPause] +[NetworkedComponent, AutoGenerateComponentPause, AutoGenerateComponentState] [RegisterComponent, Access(typeof(SharedPuddleSystem))] public sealed partial class EvaporationComponent : Component { /// /// The next time we remove the EvaporationSystem reagent amount from this entity. /// - [AutoPausedField, DataField(customTypeSerializer: typeof(TimeOffsetSerializer))] + [AutoNetworkedField, AutoPausedField] + [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))] public TimeSpan NextTick; /// diff --git a/Content.Shared/Fluids/EntitySystems/SolutionDumpingSystem.cs b/Content.Shared/Fluids/EntitySystems/SolutionDumpingSystem.cs new file mode 100644 index 0000000000..87873d50e7 --- /dev/null +++ b/Content.Shared/Fluids/EntitySystems/SolutionDumpingSystem.cs @@ -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; + +/// +/// Handles drag and drop of various solutions. +/// +/// +/// The thing dragged always "gives" its reagents away for consistent UX. +/// +/// +/// +/// +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 _itemQuery; + private EntityQuery _refillableQuery; + private EntityQuery _dumpQuery; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnDrainableCanDrag); + SubscribeLocalEvent(OnDrainableCanDragDropped); + + //SubscribeLocalEvent(OnRefillableDragged); For if you want to refill a container by dragging it into another one. Can't find a use for that currently. + SubscribeLocalEvent(OnDrainableDragged); + + SubscribeLocalEvent(OnDrainedToRefillableDragged); + SubscribeLocalEvent(OnDrainedToDumpableDragged); + + // We use queries for these since CanDropDraggedEvent gets called pretty rapidly + _itemQuery = GetEntityQuery(); + _refillableQuery = GetEntityQuery(); + _dumpQuery = GetEntityQuery(); + } + + private void OnDrainableCanDrag(Entity ent, ref CanDragEvent args) + { + if (_itemQuery.HasComp(ent)) + args.Handled = true; + } + + private void OnDrainableCanDragDropped(Entity ent, ref CanDropDraggedEvent args) + { + // Easily drawn-from thing can be dragged onto easily refillable thing. + if (!_refillableQuery.HasComp(args.Target) && !_dumpQuery.HasComp(args.Target)) + return; + + args.CanDrop = true; + args.Handled = true; + } + + /// + /// For when you are pouring something out from the container. + /// + private void OnDrainableDragged(Entity 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 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 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? 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); + } +} + +/// +/// Raised directed on a target being drained into. +/// +[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; +} diff --git a/Content.Shared/Fluids/SharedPuddleSystem.Evaporation.cs b/Content.Shared/Fluids/SharedPuddleSystem.Evaporation.cs index 8b937ed1a7..9854560204 100644 --- a/Content.Shared/Fluids/SharedPuddleSystem.Evaporation.cs +++ b/Content.Shared/Fluids/SharedPuddleSystem.Evaporation.cs @@ -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 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(uid); + evaporation.NextTick = _timing.CurTime + EvaporationCooldown; + Dirty((uid, evaporation)); + return; + } + + RemComp(uid); + } + + private void TickEvaporation() + { + var query = EntityQueryEnumerator(); + 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(); - foreach (ReagentPrototype solProto in solution.GetReagentPrototypes(_prototypeManager).Keys) + List 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(); - foreach (ReagentPrototype solProto in solution.GetReagentPrototypes(_prototypeManager).Keys) + List 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 } /// - /// 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. /// public Dictionary GetEvaporationSpeeds(Solution solution) { - var evaporatingSpeeds = new Dictionary(); - foreach (ReagentPrototype solProto in solution.GetReagentPrototypes(_prototypeManager).Keys) + Dictionary evaporatingSpeeds = []; + foreach (var solProto in solution.GetReagentPrototypes(_prototypeManager).Keys) { if (solProto.EvaporationSpeed > FixedPoint2.Zero) { diff --git a/Content.Shared/Fluids/SharedPuddleSystem.Spillable.cs b/Content.Shared/Fluids/SharedPuddleSystem.Spillable.cs index 7d65dd2424..05062aed2e 100644 --- a/Content.Shared/Fluids/SharedPuddleSystem.Spillable.cs +++ b/Content.Shared/Fluids/SharedPuddleSystem.Spillable.cs @@ -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(OnExamined); SubscribeLocalEvent>(AddSpillVerb); + SubscribeLocalEvent(SplashOnMeleeHit, after: [typeof(OpenableSystem)]); + SubscribeLocalEvent(OnAttemptPacifiedThrow); } private void OnExamined(Entity 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 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(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); + } + } + + /// + /// Prevent Pacified entities from throwing items that can spill liquids. + /// + private void OnAttemptPacifiedThrow(Entity 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"); + } } diff --git a/Content.Shared/Fluids/SharedPuddleSystem.cs b/Content.Shared/Fluids/SharedPuddleSystem.cs index 2d0fffa37b..a2ea262796 100644 --- a/Content.Shared/Fluids/SharedPuddleSystem.cs +++ b/Content.Shared/Fluids/SharedPuddleSystem.cs @@ -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 _deletionQueue = []; + + private EntityQuery _stepTriggerQuery; + private EntityQuery _reactiveQuery; + private EntityQuery _evaporationQuery; + public override void Initialize() { base.Initialize(); - SubscribeLocalEvent(OnRefillableCanDrag); - SubscribeLocalEvent(OnDumpCanDropTarget); - SubscribeLocalEvent(OnDrainCanDropTarget); - SubscribeLocalEvent(OnRefillableCanDropDragged); - + // Shouldn't need re-anchoring. + SubscribeLocalEvent(OnAnchorChanged); SubscribeLocalEvent(OnSolutionUpdate); SubscribeLocalEvent(OnGetFootstepSound); SubscribeLocalEvent(HandlePuddleExamined); SubscribeLocalEvent(OnEntRemoved); + SubscribeLocalEvent(OnEvaporationMapInit); + SubscribeLocalEvent(OnPrototypesReloaded); + _stepTriggerQuery = GetEntityQuery(); + _reactiveQuery = GetEntityQuery(); + _evaporationQuery = GetEntityQuery(); + 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()) @@ -64,44 +112,22 @@ public abstract partial class SharedPuddleSystem : EntitySystem _standoutReagents = [.. _prototypeManager.EnumeratePrototypes().Where(x => x.Standsout).Select(x => x.ID)]; } - protected virtual void OnSolutionUpdate(Entity entity, ref SolutionContainerChangedEvent args) + private void OnSolutionUpdate(Entity entity, ref SolutionContainerChangedEvent args) { if (args.SolutionId != entity.Comp.SolutionName) return; - UpdateAppearance((entity, entity.Comp)); - } - - private void OnRefillableCanDrag(Entity entity, ref CanDragEvent args) - { - args.Handled = true; - } - - private void OnDumpCanDropTarget(Entity entity, ref CanDropTargetEvent args) - { - if (HasComp(args.Dragged)) + if (args.Solution.Volume <= 0) { - args.CanDrop = true; - args.Handled = true; - } - } - - private void OnDrainCanDropTarget(Entity entity, ref CanDropTargetEvent args) - { - if (HasComp(args.Dragged)) - { - args.CanDrop = true; - args.Handled = true; - } - } - - private void OnRefillableCanDropDragged(Entity entity, ref CanDropDraggedEvent args) - { - if (!HasComp(args.Target) && !HasComp(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 entity, ref GetFootstepSoundEvent args) @@ -122,12 +148,12 @@ public abstract partial class SharedPuddleSystem : EntitySystem { using (args.PushGroup(nameof(PuddleComponent))) { - if (TryComp(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(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 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 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 entity, Solution solution) + { + if (!_stepTriggerQuery.TryComp(entity, out var comp)) + return; + + // Ensure we actually have the component + EnsureComp(entity); + EnsureComp(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(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(reagent.Prototype); + maxViscosity = Math.Max(maxViscosity, reagentProto.Viscosity); + } + + if (maxViscosity > 0) + { + var comp = EnsureComp(uid); + var speed = 1 - maxViscosity; + _speedModContacts.ChangeSpeedModifiers(uid, speed, comp); + } + else + { + RemComp(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. /// - /// 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. /// /// - /// On the client, this will always set to and return false. + /// On the client, this will always set to and return false. /// public abstract bool TrySplashSpillAt(EntityUid uid, EntityCoordinates coordinates, @@ -227,29 +375,19 @@ public abstract partial class SharedPuddleSystem : EntitySystem EntityUid? user = null); /// - /// 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. /// /// - /// On the client, this will always set to and return false. + /// On the client, this will always set to and return false. /// public abstract bool TrySpillAt(EntityCoordinates coordinates, Solution solution, out EntityUid puddleUid, bool sound = true); - /// - /// - /// - /// - /// On the client, this will always set to and return false. - /// + /// public abstract bool TrySpillAt(EntityUid uid, Solution solution, out EntityUid puddleUid, bool sound = true, TransformComponent? transformComponent = null); - /// - /// - /// - /// - /// On the client, this will always set to and return false. - /// + /// public abstract bool TrySpillAt(TileRef tileRef, Solution solution, out EntityUid puddleUid, bool sound = true, bool tileReact = true); diff --git a/Content.Shared/Nutrition/EntitySystems/OpenableSystem.cs b/Content.Shared/Nutrition/EntitySystems/OpenableSystem.cs index 04d3c20a3f..1a87dd25e9 100644 --- a/Content.Shared/Nutrition/EntitySystems/OpenableSystem.cs +++ b/Content.Shared/Nutrition/EntitySystems/OpenableSystem.cs @@ -147,18 +147,6 @@ public sealed partial class OpenableSystem : EntitySystem args.Cancelled = true; } - /// - /// 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. - /// - public bool IsOpen(EntityUid uid, OpenableComponent? comp = null) - { - if (!Resolve(uid, ref comp, false)) - return true; - - return comp.Opened; - } - /// /// 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. diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Ammunition/Projectiles/shotgun.yml b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Ammunition/Projectiles/shotgun.yml index ebea6dde5e..02ccd1a83b 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Ammunition/Projectiles/shotgun.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Ammunition/Projectiles/shotgun.yml @@ -149,15 +149,9 @@ 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 diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Basic/watergun.yml b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Basic/watergun.yml index 65535223d9..8222481e92 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Basic/watergun.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Basic/watergun.yml @@ -38,10 +38,10 @@ type: TransferAmountBoundUserInterface - type: DrawableSolution solution: chamber - - type: RefillableSolution - solution: chamber - type: DrainableSolution solution: chamber + - type: RefillableSolution + solution: chamber - type: ExaminableSolution solution: chamber - type: StaticPrice diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Projectiles/arrows.yml b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Projectiles/arrows.yml index 3f9a2d5435..0a577b51db 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Projectiles/arrows.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Projectiles/arrows.yml @@ -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 diff --git a/Resources/Prototypes/Entities/Structures/Specific/Janitor/drain.yml b/Resources/Prototypes/Entities/Structures/Specific/Janitor/drain.yml index 2a7e96ae62..770d87fd1d 100644 --- a/Resources/Prototypes/Entities/Structures/Specific/Janitor/drain.yml +++ b/Resources/Prototypes/Entities/Structures/Specific/Janitor/drain.yml @@ -38,8 +38,6 @@ solutions: drainBuffer: maxVol: 1000 - - type: DrainableSolution - solution: drainBuffer - type: Damageable damageContainer: StructuralInorganic damageModifierSet: StructuralMetallic