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 <pa.pecherskij@interfax.ru>
Co-authored-by: EmoGarbage404 <retron404@gmail.com>
This commit is contained in:
Fildrance
2025-05-26 06:36:16 +03:00
committed by GitHub
parent d1370de758
commit 291ccfbe23
14 changed files with 221 additions and 84 deletions

View File

@@ -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

View File

@@ -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<MobStateComponent>(target) || HasComp<BloodstreamComponent>(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;

View File

@@ -19,6 +19,8 @@ namespace Content.Server.Fluids.EntitySystems;
/// <inheritdoc/>
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<UseDelayComponent>(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;
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))
{
var tileRef = _mapSystem.GetTileRef(gridUid.Value, mapGrid, transform.Coordinates);
_puddleSystem.DoTileReactions(tileRef, absorberSplit);
}
_solutionContainerSystem.AddSolution(puddle.Solution.Value, absorberSplit);
}
else
{
puddleSplit = puddleSolution.SplitSolutionWithout(absorber.PickupAmount, _puddleSystem.GetAbsorbentReagents(puddleSolution));
// Despawn if we're done
if (puddleSolution.Volume == FixedPoint2.Zero)
{
// Spawn a *sparkle*
Spawn(Sparkles, GetEntityQuery<TransformComponent>().GetComponent(target).Coordinates);
QueueDel(target);
isRemoved = true;
}
}
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));
// 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);
_solutionContainerSystem.AddSolution(absorberSoln, puddleSplit);
_audio.PlayPvs(absorber.PickupSound, target);
_audio.PlayPvs(absorber.PickupSound, isRemoved ? used : target);
if (useDelay != null)
_useDelay.TryResetDelay((used, useDelay));

View File

@@ -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;
/// <summary>
/// Component for marking linked container in character slot, to which entity is bound.
/// </summary>
[RegisterComponent, Access(typeof(SlotBasedConnectedContainerSystem)), NetworkedComponent]
public sealed partial class SlotBasedConnectedContainerComponent : Component
{
/// <summary>
/// The slot in which target container should be.
/// </summary>
[DataField(required: true)]
public SlotFlags TargetSlot;
/// <summary>
/// A whitelist for determining whether container is valid or not .
/// </summary>
[DataField]
public EntityWhitelist? ContainerWhitelist;
}

View File

@@ -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<SolutionComponent>? 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;

View File

@@ -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;
/// <summary>
/// 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.
/// </summary>
public sealed class SlotBasedConnectedContainerSystem : EntitySystem
{
[Dependency] private readonly SharedContainerSystem _containers = default!;
[Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!;
[Dependency] private readonly InventorySystem _inventory = default!;
/// <inheritdoc />
public override void Initialize()
{
SubscribeLocalEvent<SlotBasedConnectedContainerComponent, GetConnectedContainerEvent>(OnGettingConnectedContainer);
}
/// <summary>
/// Try get connected container entity in character slots for <see cref="uid"/>.
/// </summary>
/// <param name="uid">
/// Entity for which connected container is required. If <see cref="SlotBasedConnectedContainerComponent"/>
/// is used - tries to find container in slot, returns false and null <see cref="slotEntity"/> otherwise.
/// </param>
/// <param name="slotEntity">Found connected container entity or null.</param>
/// <returns>True if connected container was found, false otherwise.</returns>
public bool TryGetConnectedContainer(EntityUid uid, [NotNullWhen(true)] out EntityUid? slotEntity)
{
if (!TryComp<SlotBasedConnectedContainerComponent>(uid, out var component))
{
slotEntity = null;
return false;
}
return TryGetConnectedContainer(uid, component.TargetSlot, component.ContainerWhitelist, out slotEntity);
}
private void OnGettingConnectedContainer(Entity<SlotBasedConnectedContainerComponent> 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;
}
}
/// <summary>
/// Event for an attempt of getting container, connected to entity on which event was raised.
/// Fills <see cref="ContainerEntity"/> if connected container exists.
/// </summary>
[ByRefEvent]
public struct GetConnectedContainerEvent
{
/// <summary>
/// Container entity, if it exists, or null.
/// </summary>
public EntityUid? ContainerEntity;
}

View File

@@ -11,23 +11,28 @@ namespace Content.Shared.Fluids;
[RegisterComponent, NetworkedComponent]
public sealed partial class AbsorbentComponent : Component
{
public const string SolutionName = "absorbed";
public Dictionary<Color, float> Progress = new();
/// <summary>
/// Name for solution container, that should be used for absorbed solution storage and as source of absorber solution.
/// Default is 'absorbed'.
/// </summary>
[DataField]
public string SolutionName = "absorbed";
/// <summary>
/// How much solution we can transfer in one interaction.
/// </summary>
[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),
};
/// <summary>
/// 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.
/// </summary>
[DataField]
public bool UseAbsorberSolution = true;
}

View File

@@ -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;

View File

@@ -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.
/// </summary>
[RegisterComponent, NetworkedComponent, Access(typeof(SharedGunSystem))]
public sealed partial class ClothingSlotAmmoProviderComponent : AmmoProviderComponent
{
/// <summary>
/// The slot that the ammo provider should be located in.
/// </summary>
[DataField("targetSlot", required: true)]
public SlotFlags TargetSlot;
/// <summary>
/// A whitelist for determining whether or not an ammo provider is valid.
/// </summary>
[DataField("providerWhitelist")]
public EntityWhitelist? ProviderWhitelist;
}
public sealed partial class ClothingSlotAmmoProviderComponent : AmmoProviderComponent;

View File

@@ -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<ClothingSlotAmmoProviderComponent, TakeAmmoEvent>(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);
}
}

View File

@@ -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/"

Binary file not shown.

View File

@@ -34,6 +34,7 @@
size: Large
sprite: Objects/Specific/Janitorial/mop.rsi
- type: Absorbent
useAbsorberSolution: true
- type: SolutionContainerManager
solutions:
absorbed:
@@ -89,6 +90,7 @@
sprite: Objects/Specific/Janitorial/advmop.rsi
- type: Absorbent
pickupAmount: 100
useAbsorberSolution: true
- type: UseDelay
delay: 1.0
- type: SolutionRegeneration
@@ -322,6 +324,7 @@
sprite: Objects/Specific/Janitorial/rag.rsi
- type: Absorbent
pickupAmount: 15
useAbsorberSolution: true
- type: Construction
graph: Rag
node: rag

View File

@@ -21,9 +21,18 @@
- FullAuto
soundGunshot:
path: /Audio/Weapons/Guns/Gunshots/water_spray.ogg
- type: Absorbent
pickupAmount: 35
solutionName: tank
useAbsorberSolution: false
pickupSound:
path: /Audio/Effects/Fluids/vacuum-cleaner-fast.ogg
- type: Appearance
- type: ClothingSlotAmmoProvider
- type: SlotBasedConnectedContainer
targetSlot: BACK
providerWhitelist:
containerWhitelist:
tags:
- NozzleBackTank
- type: UseDelay
delay: 1.0