From 291ccfbe23506679e44345ee1dab0070e14da47e Mon Sep 17 00:00:00 2001 From: Fildrance Date: Mon, 26 May 2025 06:36:16 +0300 Subject: [PATCH] Spray nozzle can suck puddles into tank directly! (#30600) * feat: now vacuum cleaner can suck solutions from floor * refactor using AbsorbentSystem instead of separate vacuum cleaner * refactor: remove unused vacuum cleaner files * refactor: renamed ConnectedContainerComponent to SlotBasedConnectedContainerComponent (and system) * fix: fix invalid comp name * fix: no more spray nozzle messaging about water inside bottles etc. * refactor: minor refactor in SlotBasedConnectedContainerSystem and adjustments after merge * refactor: cleanups * refactor: renaming * refactor: update to use _puddleSystem.GetAbsorbentReagents * refactor: changed interactions with SlotBasedConnectedContainerSystem into events * refactor: new sound and action delay adjusted to sound (amount tweaked a bit accordingly, almost) * refactor: added networking for SlotBasedConnectedContainerComponent * fix attribution for vacuum-cleaner-fast.ogg * trying to fix multi-license for mix sound file * remove empty line * refactor: remove trailing whitespace * by ref struct, brother --------- Co-authored-by: pa.pecherskij Co-authored-by: EmoGarbage404 --- .../Tests/Fluids/AbsorbentTest.cs | 5 +- .../Chemistry/EntitySystems/InjectorSystem.cs | 2 +- .../Fluids/EntitySystems/AbsorbentSystem.cs | 67 +++++++++----- .../SlotBasedConnectedContainerComponent.cs | 25 +++++ .../SharedSolutionContainerSystem.cs | 7 ++ .../SlotBasedConnectedContainerSystem.cs | 86 ++++++++++++++++++ Content.Shared/Fluids/AbsorbentComponent.cs | 22 ++++- .../Components/AmmoProviderComponent.cs | 2 +- .../ClothingSlotAmmoProviderComponent.cs | 17 +--- .../Systems/SharedGunSystem.Clothing.cs | 40 ++------ .../Audio/Effects/Fluids/attributions.yml | 10 ++ .../Effects/Fluids/vacuum-cleaner-fast.ogg | Bin 0 -> 13296 bytes .../Objects/Specific/Janitorial/janitor.yml | 3 + .../Weapons/Guns/Basic/spraynozzle.yml | 11 ++- 14 files changed, 217 insertions(+), 80 deletions(-) create mode 100644 Content.Shared/Chemistry/Components/SlotBasedConnectedContainerComponent.cs create mode 100644 Content.Shared/Containers/SlotBasedConnectedContainerSystem.cs create mode 100644 Resources/Audio/Effects/Fluids/vacuum-cleaner-fast.ogg diff --git a/Content.IntegrationTests/Tests/Fluids/AbsorbentTest.cs b/Content.IntegrationTests/Tests/Fluids/AbsorbentTest.cs index 87ef41fe96..bf47768274 100644 --- a/Content.IntegrationTests/Tests/Fluids/AbsorbentTest.cs +++ b/Content.IntegrationTests/Tests/Fluids/AbsorbentTest.cs @@ -33,6 +33,7 @@ public sealed class AbsorbentTest id: {AbsorbentDummyId} components: - type: Absorbent + useAbsorberSolution: true - type: SolutionContainerManager solutions: absorbed: @@ -94,7 +95,7 @@ public sealed class AbsorbentTest refillable = entityManager.SpawnEntity(RefillableDummyId, coordinates); entityManager.TryGetComponent(absorbent, out component); - solutionContainerSystem.TryGetSolution(absorbent, AbsorbentComponent.SolutionName, out var absorbentSoln, out var absorbentSolution); + solutionContainerSystem.TryGetSolution(absorbent, component.SolutionName, out var absorbentSoln, out var absorbentSolution); solutionContainerSystem.TryGetRefillableSolution(refillable, out var refillableSoln, out var refillableSolution); // Arrange @@ -152,7 +153,7 @@ public sealed class AbsorbentTest refillable = entityManager.SpawnEntity(SmallRefillableDummyId, coordinates); entityManager.TryGetComponent(absorbent, out component); - solutionContainerSystem.TryGetSolution(absorbent, AbsorbentComponent.SolutionName, out var absorbentSoln, out var absorbentSolution); + solutionContainerSystem.TryGetSolution(absorbent, component.SolutionName, out var absorbentSoln, out var absorbentSolution); solutionContainerSystem.TryGetRefillableSolution(refillable, out var refillableSoln, out var refillableSolution); // Arrange diff --git a/Content.Server/Chemistry/EntitySystems/InjectorSystem.cs b/Content.Server/Chemistry/EntitySystems/InjectorSystem.cs index cc32d5a245..62303c2e35 100644 --- a/Content.Server/Chemistry/EntitySystems/InjectorSystem.cs +++ b/Content.Server/Chemistry/EntitySystems/InjectorSystem.cs @@ -91,7 +91,7 @@ public sealed class InjectorSystem : SharedInjectorSystem // Is the target a mob? If yes, use a do-after to give them time to respond. if (HasComp(target) || HasComp(target)) { - // Are use using an injector capible of targeting a mob? + // Are use using an injector capable of targeting a mob? if (entity.Comp.IgnoreMobs) return; diff --git a/Content.Server/Fluids/EntitySystems/AbsorbentSystem.cs b/Content.Server/Fluids/EntitySystems/AbsorbentSystem.cs index cb3cae10af..1177c24304 100644 --- a/Content.Server/Fluids/EntitySystems/AbsorbentSystem.cs +++ b/Content.Server/Fluids/EntitySystems/AbsorbentSystem.cs @@ -19,6 +19,8 @@ namespace Content.Server.Fluids.EntitySystems; /// public sealed class AbsorbentSystem : SharedAbsorbentSystem { + private static readonly EntProtoId Sparkles = "PuddleSparkle"; + [Dependency] private readonly IPrototypeManager _prototype = default!; [Dependency] private readonly AudioSystem _audio = default!; [Dependency] private readonly PopupSystem _popups = default!; @@ -51,7 +53,7 @@ public sealed class AbsorbentSystem : SharedAbsorbentSystem private void UpdateAbsorbent(EntityUid uid, AbsorbentComponent component) { - if (!_solutionContainerSystem.TryGetSolution(uid, AbsorbentComponent.SolutionName, out _, out var solution)) + if (!_solutionContainerSystem.TryGetSolution(uid, component.SolutionName, out _, out var solution)) return; var oldProgress = component.Progress.ShallowClone(); @@ -104,7 +106,7 @@ public sealed class AbsorbentSystem : SharedAbsorbentSystem public void Mop(EntityUid user, EntityUid target, EntityUid used, AbsorbentComponent component) { - if (!_solutionContainerSystem.TryGetSolution(used, AbsorbentComponent.SolutionName, out var absorberSoln)) + if (!_solutionContainerSystem.TryGetSolution(used, component.SolutionName, out var absorberSoln)) return; if (TryComp(used, out var useDelay) @@ -112,7 +114,7 @@ public sealed class AbsorbentSystem : SharedAbsorbentSystem return; // If it's a puddle try to grab from - if (!TryPuddleInteract(user, used, target, component, useDelay, absorberSoln.Value)) + if (!TryPuddleInteract(user, used, target, component, useDelay, absorberSoln.Value) && component.UseAbsorberSolution) { // If it's refillable try to transfer if (!TryRefillableInteract(user, used, target, component, useDelay, absorberSoln.Value)) @@ -282,36 +284,53 @@ public sealed class AbsorbentSystem : SharedAbsorbentSystem return true; } - // Check if we have any evaporative reagents on our absorber to transfer - var absorberSolution = absorberSoln.Comp.Solution; - var available = absorberSolution.GetTotalPrototypeQuantity(_puddleSystem.GetAbsorbentReagents(absorberSolution)); - - // No material - if (available == FixedPoint2.Zero) + Solution puddleSplit; + var isRemoved = false; + if (absorber.UseAbsorberSolution) { - _popups.PopupEntity(Loc.GetString("mopping-system-no-water", ("used", used)), user, user); - return true; - } + // Check if we have any evaporative reagents on our absorber to transfer + var absorberSolution = absorberSoln.Comp.Solution; + var available = absorberSolution.GetTotalPrototypeQuantity(_puddleSystem.GetAbsorbentReagents(absorberSolution)); + + // No material + if (available == FixedPoint2.Zero) + { + _popups.PopupEntity(Loc.GetString("mopping-system-no-water", ("used", used)), user, user); + return true; + } - var transferMax = absorber.PickupAmount; - var transferAmount = available > transferMax ? transferMax : available; + var transferMax = absorber.PickupAmount; + var transferAmount = available > transferMax ? transferMax : available; - var puddleSplit = puddleSolution.SplitSolutionWithout(transferAmount, _puddleSystem.GetAbsorbentReagents(puddleSolution)); - var absorberSplit = absorberSolution.SplitSolutionWithOnly(puddleSplit.Volume, _puddleSystem.GetAbsorbentReagents(absorberSolution)); + puddleSplit = puddleSolution.SplitSolutionWithout(transferAmount, _puddleSystem.GetAbsorbentReagents(puddleSolution)); + var absorberSplit = absorberSolution.SplitSolutionWithOnly(puddleSplit.Volume, _puddleSystem.GetAbsorbentReagents(absorberSolution)); - // Do tile reactions first - var transform = Transform(target); - var gridUid = transform.GridUid; - if (TryComp(gridUid, out MapGridComponent? mapGrid)) + // Do tile reactions first + var transform = Transform(target); + var gridUid = transform.GridUid; + if (TryComp(gridUid, out MapGridComponent? mapGrid)) + { + var tileRef = _mapSystem.GetTileRef(gridUid.Value, mapGrid, transform.Coordinates); + _puddleSystem.DoTileReactions(tileRef, absorberSplit); + } + _solutionContainerSystem.AddSolution(puddle.Solution.Value, absorberSplit); + } + else { - var tileRef = _mapSystem.GetTileRef(gridUid.Value, mapGrid, transform.Coordinates); - _puddleSystem.DoTileReactions(tileRef, absorberSplit); + puddleSplit = puddleSolution.SplitSolutionWithout(absorber.PickupAmount, _puddleSystem.GetAbsorbentReagents(puddleSolution)); + // Despawn if we're done + if (puddleSolution.Volume == FixedPoint2.Zero) + { + // Spawn a *sparkle* + Spawn(Sparkles, GetEntityQuery().GetComponent(target).Coordinates); + QueueDel(target); + isRemoved = true; + } } - _solutionContainerSystem.AddSolution(puddle.Solution.Value, absorberSplit); _solutionContainerSystem.AddSolution(absorberSoln, puddleSplit); - _audio.PlayPvs(absorber.PickupSound, target); + _audio.PlayPvs(absorber.PickupSound, isRemoved ? used : target); if (useDelay != null) _useDelay.TryResetDelay((used, useDelay)); diff --git a/Content.Shared/Chemistry/Components/SlotBasedConnectedContainerComponent.cs b/Content.Shared/Chemistry/Components/SlotBasedConnectedContainerComponent.cs new file mode 100644 index 0000000000..2fde557941 --- /dev/null +++ b/Content.Shared/Chemistry/Components/SlotBasedConnectedContainerComponent.cs @@ -0,0 +1,25 @@ +using Content.Shared.Containers; +using Content.Shared.Inventory; +using Content.Shared.Whitelist; +using Robust.Shared.GameStates; + +namespace Content.Shared.Chemistry.Components; + +/// +/// Component for marking linked container in character slot, to which entity is bound. +/// +[RegisterComponent, Access(typeof(SlotBasedConnectedContainerSystem)), NetworkedComponent] +public sealed partial class SlotBasedConnectedContainerComponent : Component +{ + /// + /// The slot in which target container should be. + /// + [DataField(required: true)] + public SlotFlags TargetSlot; + + /// + /// A whitelist for determining whether container is valid or not . + /// + [DataField] + public EntityWhitelist? ContainerWhitelist; +} diff --git a/Content.Shared/Chemistry/EntitySystems/SharedSolutionContainerSystem.cs b/Content.Shared/Chemistry/EntitySystems/SharedSolutionContainerSystem.cs index 99d1459340..f536beef2b 100644 --- a/Content.Shared/Chemistry/EntitySystems/SharedSolutionContainerSystem.cs +++ b/Content.Shared/Chemistry/EntitySystems/SharedSolutionContainerSystem.cs @@ -14,6 +14,7 @@ using System.Linq; using System.Numerics; using System.Runtime.CompilerServices; using System.Text; +using Content.Shared.Containers; using Content.Shared.Hands.Components; using Content.Shared.Hands.EntitySystems; using Robust.Shared.Map; @@ -162,6 +163,12 @@ public abstract partial class SharedSolutionContainerSystem : EntitySystem [NotNullWhen(true)] out Entity? entity, bool errorOnMissing = false) { + // use connected container instead of entity from arguments, if it exists. + var ev = new GetConnectedContainerEvent(); + RaiseLocalEvent(container, ref ev); + if (ev.ContainerEntity.HasValue) + container = ev.ContainerEntity.Value; + EntityUid uid; if (name is null) uid = container; diff --git a/Content.Shared/Containers/SlotBasedConnectedContainerSystem.cs b/Content.Shared/Containers/SlotBasedConnectedContainerSystem.cs new file mode 100644 index 0000000000..4970a54e33 --- /dev/null +++ b/Content.Shared/Containers/SlotBasedConnectedContainerSystem.cs @@ -0,0 +1,86 @@ +using System.Diagnostics.CodeAnalysis; +using Content.Shared.Chemistry.Components; +using Content.Shared.Inventory; +using Content.Shared.Whitelist; +using Robust.Shared.Containers; + +namespace Content.Shared.Containers; + +/// +/// System for getting container that is linked to subject entity. Container is supposed to be present in certain character slot. +/// Can be used for linking ammo feeder, solution source for spray nozzle, etc. +/// +public sealed class SlotBasedConnectedContainerSystem : EntitySystem +{ + [Dependency] private readonly SharedContainerSystem _containers = default!; + [Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!; + [Dependency] private readonly InventorySystem _inventory = default!; + + /// + public override void Initialize() + { + SubscribeLocalEvent(OnGettingConnectedContainer); + } + + /// + /// Try get connected container entity in character slots for . + /// + /// + /// Entity for which connected container is required. If + /// is used - tries to find container in slot, returns false and null otherwise. + /// + /// Found connected container entity or null. + /// True if connected container was found, false otherwise. + public bool TryGetConnectedContainer(EntityUid uid, [NotNullWhen(true)] out EntityUid? slotEntity) + { + if (!TryComp(uid, out var component)) + { + slotEntity = null; + return false; + } + + return TryGetConnectedContainer(uid, component.TargetSlot, component.ContainerWhitelist, out slotEntity); + } + + private void OnGettingConnectedContainer(Entity ent, ref GetConnectedContainerEvent args) + { + if (TryGetConnectedContainer(ent, ent.Comp.TargetSlot, ent.Comp.ContainerWhitelist, out var val)) + args.ContainerEntity = val; + } + + private bool TryGetConnectedContainer(EntityUid uid, SlotFlags slotFlag, EntityWhitelist? providerWhitelist, [NotNullWhen(true)] out EntityUid? slotEntity) + { + slotEntity = null; + + if (!_containers.TryGetContainingContainer((uid, null, null), out var container)) + return false; + + var user = container.Owner; + if (!_inventory.TryGetContainerSlotEnumerator(user, out var enumerator, slotFlag)) + return false; + + while (enumerator.NextItem(out var item)) + { + if (_whitelistSystem.IsWhitelistFailOrNull(providerWhitelist, item)) + continue; + + slotEntity = item; + return true; + } + + return false; + } +} + +/// +/// Event for an attempt of getting container, connected to entity on which event was raised. +/// Fills if connected container exists. +/// +[ByRefEvent] +public struct GetConnectedContainerEvent +{ + /// + /// Container entity, if it exists, or null. + /// + public EntityUid? ContainerEntity; +} diff --git a/Content.Shared/Fluids/AbsorbentComponent.cs b/Content.Shared/Fluids/AbsorbentComponent.cs index 450ecc0df6..2d1a922381 100644 --- a/Content.Shared/Fluids/AbsorbentComponent.cs +++ b/Content.Shared/Fluids/AbsorbentComponent.cs @@ -11,23 +11,28 @@ namespace Content.Shared.Fluids; [RegisterComponent, NetworkedComponent] public sealed partial class AbsorbentComponent : Component { - public const string SolutionName = "absorbed"; - public Dictionary Progress = new(); + /// + /// Name for solution container, that should be used for absorbed solution storage and as source of absorber solution. + /// Default is 'absorbed'. + /// + [DataField] + public string SolutionName = "absorbed"; + /// /// How much solution we can transfer in one interaction. /// - [DataField("pickupAmount")] + [DataField] public FixedPoint2 PickupAmount = FixedPoint2.New(100); - [DataField("pickupSound")] + [DataField] public SoundSpecifier PickupSound = new SoundPathSpecifier("/Audio/Effects/Fluids/watersplash.ogg") { Params = AudioParams.Default.WithVariation(SharedContentAudioSystem.DefaultVariation), }; - [DataField("transferSound")] public SoundSpecifier TransferSound = + [DataField] public SoundSpecifier TransferSound = new SoundPathSpecifier("/Audio/Effects/Fluids/slosh.ogg") { Params = AudioParams.Default.WithVariation(SharedContentAudioSystem.DefaultVariation).WithVolume(-3f), @@ -38,4 +43,11 @@ public sealed partial class AbsorbentComponent : Component { Params = AudioParams.Default.WithVariation(SharedContentAudioSystem.DefaultVariation).WithVolume(-3f), }; + + /// + /// Marker that absorbent component owner should try to use 'absorber solution' to replace solution to be absorbed. + /// Target solution will be simply consumed into container if set to false. + /// + [DataField] + public bool UseAbsorberSolution = true; } diff --git a/Content.Shared/Weapons/Ranged/Components/AmmoProviderComponent.cs b/Content.Shared/Weapons/Ranged/Components/AmmoProviderComponent.cs index 9a7fdc07de..8e2f767f4d 100644 --- a/Content.Shared/Weapons/Ranged/Components/AmmoProviderComponent.cs +++ b/Content.Shared/Weapons/Ranged/Components/AmmoProviderComponent.cs @@ -3,4 +3,4 @@ using Robust.Shared.GameStates; namespace Content.Shared.Weapons.Ranged.Components; [NetworkedComponent] -public abstract partial class AmmoProviderComponent : Component {} +public abstract partial class AmmoProviderComponent : Component; diff --git a/Content.Shared/Weapons/Ranged/Components/ClothingSlotAmmoProviderComponent.cs b/Content.Shared/Weapons/Ranged/Components/ClothingSlotAmmoProviderComponent.cs index e363fc9fe1..418e712c7b 100644 --- a/Content.Shared/Weapons/Ranged/Components/ClothingSlotAmmoProviderComponent.cs +++ b/Content.Shared/Weapons/Ranged/Components/ClothingSlotAmmoProviderComponent.cs @@ -1,6 +1,4 @@ -using Content.Shared.Inventory; using Content.Shared.Weapons.Ranged.Systems; -using Content.Shared.Whitelist; using Robust.Shared.GameStates; namespace Content.Shared.Weapons.Ranged.Components; @@ -10,17 +8,4 @@ namespace Content.Shared.Weapons.Ranged.Components; /// to an entity in the user's clothing slot. /// [RegisterComponent, NetworkedComponent, Access(typeof(SharedGunSystem))] -public sealed partial class ClothingSlotAmmoProviderComponent : AmmoProviderComponent -{ - /// - /// The slot that the ammo provider should be located in. - /// - [DataField("targetSlot", required: true)] - public SlotFlags TargetSlot; - - /// - /// A whitelist for determining whether or not an ammo provider is valid. - /// - [DataField("providerWhitelist")] - public EntityWhitelist? ProviderWhitelist; -} +public sealed partial class ClothingSlotAmmoProviderComponent : AmmoProviderComponent; diff --git a/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.Clothing.cs b/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.Clothing.cs index 7ef57df539..12bbe0c312 100644 --- a/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.Clothing.cs +++ b/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.Clothing.cs @@ -1,5 +1,4 @@ -using System.Diagnostics.CodeAnalysis; -using Content.Shared.Inventory; +using Content.Shared.Containers; using Content.Shared.Weapons.Ranged.Components; using Content.Shared.Weapons.Ranged.Events; @@ -7,8 +6,6 @@ namespace Content.Shared.Weapons.Ranged.Systems; public partial class SharedGunSystem { - [Dependency] private readonly InventorySystem _inventory = default!; - private void InitializeClothing() { SubscribeLocalEvent(OnClothingTakeAmmo); @@ -17,38 +14,21 @@ public partial class SharedGunSystem private void OnClothingTakeAmmo(EntityUid uid, ClothingSlotAmmoProviderComponent component, TakeAmmoEvent args) { - if (!TryGetClothingSlotEntity(uid, component, out var entity)) + var getConnectedContainerEvent = new GetConnectedContainerEvent(); + RaiseLocalEvent(uid, ref getConnectedContainerEvent); + if(!getConnectedContainerEvent.ContainerEntity.HasValue) return; - RaiseLocalEvent(entity.Value, args); + + RaiseLocalEvent(getConnectedContainerEvent.ContainerEntity.Value, args); } private void OnClothingAmmoCount(EntityUid uid, ClothingSlotAmmoProviderComponent component, ref GetAmmoCountEvent args) { - if (!TryGetClothingSlotEntity(uid, component, out var entity)) + var getConnectedContainerEvent = new GetConnectedContainerEvent(); + RaiseLocalEvent(uid, ref getConnectedContainerEvent); + if (!getConnectedContainerEvent.ContainerEntity.HasValue) return; - RaiseLocalEvent(entity.Value, ref args); - } - - private bool TryGetClothingSlotEntity(EntityUid uid, ClothingSlotAmmoProviderComponent component, [NotNullWhen(true)] out EntityUid? slotEntity) - { - slotEntity = null; - - if (!Containers.TryGetContainingContainer((uid, null, null), out var container)) - return false; - var user = container.Owner; - - if (!_inventory.TryGetContainerSlotEnumerator(user, out var enumerator, component.TargetSlot)) - return false; - - while (enumerator.NextItem(out var item)) - { - if (_whitelistSystem.IsWhitelistFailOrNull(component.ProviderWhitelist, item)) - continue; - - slotEntity = item; - return true; - } - return false; + RaiseLocalEvent(getConnectedContainerEvent.ContainerEntity.Value, ref args); } } diff --git a/Resources/Audio/Effects/Fluids/attributions.yml b/Resources/Audio/Effects/Fluids/attributions.yml index aebe3e3c3f..c23a5f5c7f 100644 --- a/Resources/Audio/Effects/Fluids/attributions.yml +++ b/Resources/Audio/Effects/Fluids/attributions.yml @@ -22,3 +22,13 @@ license: "CC-BY-SA-3.0" copyright: "Created by the_toilet_guy" source: "https://freesound.org/people/the_toilet_guy/sounds/98770/" + +- files: ["vacuum-cleaner-fast.ogg"] + license: "CC0-1.0" + copyright: "Created by kyles" + source: "https://freesound.org/people/kyles/sounds/637927/" + +- files: ["vacuum-cleaner-fast.ogg"] + license: "CC0-1.0" + copyright: "Created by BrendanSound12 mixed by Fildrance" + source: "https://freesound.org/people/BrendanSound12/sounds/445165/" diff --git a/Resources/Audio/Effects/Fluids/vacuum-cleaner-fast.ogg b/Resources/Audio/Effects/Fluids/vacuum-cleaner-fast.ogg new file mode 100644 index 0000000000000000000000000000000000000000..bf20520b238995ac0eddd16b7f66ec36b4d34551 GIT binary patch literal 13296 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