Add prediction to Spill Container verb, add dummy TrySpill methods to shared (#25813)

* Moved abstract spill methods to shared; added prediction to spill container verb.

* Rerun tests

* Requested changes

* Note Client behavior in Spill method docs
This commit is contained in:
Tayrtahn
2024-03-29 05:00:09 -04:00
committed by GitHub
parent 19caf1d9d3
commit 4cd2fbd076
8 changed files with 178 additions and 87 deletions

View File

@@ -1,7 +1,9 @@
using Content.Client.IconSmoothing; using Content.Client.IconSmoothing;
using Content.Shared.Chemistry.Components;
using Content.Shared.Fluids; using Content.Shared.Fluids;
using Content.Shared.Fluids.Components; using Content.Shared.Fluids.Components;
using Robust.Client.GameObjects; using Robust.Client.GameObjects;
using Robust.Shared.Map;
namespace Content.Client.Fluids; namespace Content.Client.Fluids;
@@ -21,7 +23,7 @@ public sealed class PuddleSystem : SharedPuddleSystem
if (args.Sprite == null) if (args.Sprite == null)
return; return;
float volume = 1f; var volume = 1f;
if (args.AppearanceData.TryGetValue(PuddleVisuals.CurrentVolume, out var volumeObj)) if (args.AppearanceData.TryGetValue(PuddleVisuals.CurrentVolume, out var volumeObj))
{ {
@@ -64,4 +66,38 @@ public sealed class PuddleSystem : SharedPuddleSystem
args.Sprite.Color *= baseColor; args.Sprite.Color *= baseColor;
} }
} }
#region Spill
// Maybe someday we'll have clientside prediction for entity spawning, but not today.
// Until then, these methods do nothing on the client.
/// <inheritdoc/>
public override bool TrySplashSpillAt(EntityUid uid, EntityCoordinates coordinates, Solution solution, out EntityUid puddleUid, bool sound = true, EntityUid? user = null)
{
puddleUid = EntityUid.Invalid;
return false;
}
/// <inheritdoc/>
public override bool TrySpillAt(EntityCoordinates coordinates, Solution solution, out EntityUid puddleUid, bool sound = true)
{
puddleUid = EntityUid.Invalid;
return false;
}
/// <inheritdoc/>
public override bool TrySpillAt(EntityUid uid, Solution solution, out EntityUid puddleUid, bool sound = true, TransformComponent? transformComponent = null)
{
puddleUid = EntityUid.Invalid;
return false;
}
/// <inheritdoc/>
public override bool TrySpillAt(TileRef tileRef, Solution solution, out EntityUid puddleUid, bool sound = true, bool tileReact = true)
{
puddleUid = EntityUid.Invalid;
return false;
}
#endregion Spill
} }

View File

@@ -1,7 +0,0 @@
namespace Content.Server.Fluids.Components;
[RegisterComponent]
public sealed partial class PreventSpillerComponent : Component
{
}

View File

@@ -1,5 +1,4 @@
using Content.Server.Chemistry.Containers.EntitySystems; using Content.Server.Chemistry.Containers.EntitySystems;
using Content.Server.Fluids.Components;
using Content.Server.Nutrition.EntitySystems; using Content.Server.Nutrition.EntitySystems;
using Content.Shared.Chemistry.Components; using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.EntitySystems; using Content.Shared.Chemistry.EntitySystems;
@@ -8,7 +7,6 @@ using Content.Shared.Chemistry.Reagent;
using Content.Shared.Clothing.Components; using Content.Shared.Clothing.Components;
using Content.Shared.CombatMode.Pacification; using Content.Shared.CombatMode.Pacification;
using Content.Shared.Database; using Content.Shared.Database;
using Content.Shared.DoAfter;
using Content.Shared.FixedPoint; using Content.Shared.FixedPoint;
using Content.Shared.Fluids.Components; using Content.Shared.Fluids.Components;
using Content.Shared.IdentityManagement; using Content.Shared.IdentityManagement;
@@ -16,7 +14,6 @@ using Content.Shared.Inventory.Events;
using Content.Shared.Popups; using Content.Shared.Popups;
using Content.Shared.Spillable; using Content.Shared.Spillable;
using Content.Shared.Throwing; using Content.Shared.Throwing;
using Content.Shared.Verbs;
using Content.Shared.Weapons.Melee.Events; using Content.Shared.Weapons.Melee.Events;
using Robust.Shared.Player; using Robust.Shared.Player;
@@ -24,9 +21,6 @@ namespace Content.Server.Fluids.EntitySystems;
public sealed partial class PuddleSystem public sealed partial class PuddleSystem
{ {
[Dependency] private readonly OpenableSystem _openable = default!;
[Dependency] private readonly IEntityManager _entityManager = default!;
protected override void InitializeSpillable() protected override void InitializeSpillable()
{ {
base.InitializeSpillable(); base.InitializeSpillable();
@@ -34,7 +28,6 @@ public sealed partial class PuddleSystem
SubscribeLocalEvent<SpillableComponent, LandEvent>(SpillOnLand); SubscribeLocalEvent<SpillableComponent, LandEvent>(SpillOnLand);
// Openable handles the event if it's closed // Openable handles the event if it's closed
SubscribeLocalEvent<SpillableComponent, MeleeHitEvent>(SplashOnMeleeHit, after: [typeof(OpenableSystem)]); SubscribeLocalEvent<SpillableComponent, MeleeHitEvent>(SplashOnMeleeHit, after: [typeof(OpenableSystem)]);
SubscribeLocalEvent<SpillableComponent, GetVerbsEvent<Verb>>(AddSpillVerb);
SubscribeLocalEvent<SpillableComponent, GotEquippedEvent>(OnGotEquipped); SubscribeLocalEvent<SpillableComponent, GotEquippedEvent>(OnGotEquipped);
SubscribeLocalEvent<SpillableComponent, SolutionContainerOverflowEvent>(OnOverflow); SubscribeLocalEvent<SpillableComponent, SolutionContainerOverflowEvent>(OnOverflow);
SubscribeLocalEvent<SpillableComponent, SpillDoAfterEvent>(OnDoAfter); SubscribeLocalEvent<SpillableComponent, SpillDoAfterEvent>(OnDoAfter);
@@ -134,7 +127,7 @@ public sealed partial class PuddleSystem
if (!_solutionContainerSystem.TryGetSolution(entity.Owner, entity.Comp.SolutionName, out var soln, out var solution)) if (!_solutionContainerSystem.TryGetSolution(entity.Owner, entity.Comp.SolutionName, out var soln, out var solution))
return; return;
if (_openable.IsClosed(entity.Owner)) if (Openable.IsClosed(entity.Owner))
return; return;
if (args.User != null) if (args.User != null)
@@ -153,7 +146,7 @@ public sealed partial class PuddleSystem
private void OnAttemptPacifiedThrow(Entity<SpillableComponent> ent, ref AttemptPacifiedThrowEvent args) private void OnAttemptPacifiedThrow(Entity<SpillableComponent> ent, ref AttemptPacifiedThrowEvent args)
{ {
// Dont care about closed containers. // Dont care about closed containers.
if (_openable.IsClosed(ent)) if (Openable.IsClosed(ent))
return; return;
// Dont care about empty containers. // Dont care about empty containers.
@@ -163,57 +156,6 @@ public sealed partial class PuddleSystem
args.Cancel("pacified-cannot-throw-spill"); args.Cancel("pacified-cannot-throw-spill");
} }
private void AddSpillVerb(Entity<SpillableComponent> entity, ref GetVerbsEvent<Verb> args)
{
if (!args.CanAccess || !args.CanInteract)
return;
if (!_solutionContainerSystem.TryGetSolution(args.Target, entity.Comp.SolutionName, out var soln, out var solution))
return;
if (_openable.IsClosed(args.Target))
return;
if (solution.Volume == FixedPoint2.Zero)
return;
if (_entityManager.HasComponent<PreventSpillerComponent>(args.User))
return;
Verb verb = new()
{
Text = Loc.GetString("spill-target-verb-get-data-text")
};
// TODO VERB ICONS spill icon? pouring out a glass/beaker?
if (entity.Comp.SpillDelay == null)
{
var target = args.Target;
verb.Act = () =>
{
var puddleSolution = _solutionContainerSystem.SplitSolution(soln.Value, solution.Volume);
TrySpillAt(Transform(target).Coordinates, puddleSolution, out _);
};
}
else
{
var user = args.User;
verb.Act = () =>
{
_doAfterSystem.TryStartDoAfter(new DoAfterArgs(EntityManager, user, entity.Comp.SpillDelay ?? 0, new SpillDoAfterEvent(), entity.Owner, target: entity.Owner)
{
BreakOnDamage = true,
BreakOnMove = true,
NeedHand = true,
});
};
}
verb.Impact = LogImpact.Medium; // dangerous reagent reaction are logged separately.
verb.DoContactInteraction = true;
args.Verbs.Add(verb);
}
private void OnDoAfter(Entity<SpillableComponent> entity, ref SpillDoAfterEvent args) private void OnDoAfter(Entity<SpillableComponent> entity, ref SpillDoAfterEvent args)
{ {
if (args.Handled || args.Cancelled || args.Args.Target == null) if (args.Handled || args.Cancelled || args.Args.Target == null)

View File

@@ -46,7 +46,6 @@ public sealed partial class PuddleSystem : SharedPuddleSystem
[Dependency] private readonly IRobustRandom _random = default!; [Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly ITileDefinitionManager _tileDefMan = default!; [Dependency] private readonly ITileDefinitionManager _tileDefMan = default!;
[Dependency] private readonly AudioSystem _audio = default!; [Dependency] private readonly AudioSystem _audio = default!;
[Dependency] private readonly DoAfterSystem _doAfterSystem = default!;
[Dependency] private readonly EntityLookupSystem _lookup = default!; [Dependency] private readonly EntityLookupSystem _lookup = default!;
[Dependency] private readonly ReactiveSystem _reactive = default!; [Dependency] private readonly ReactiveSystem _reactive = default!;
[Dependency] private readonly SharedAppearanceSystem _appearance = default!; [Dependency] private readonly SharedAppearanceSystem _appearance = default!;
@@ -551,11 +550,8 @@ public sealed partial class PuddleSystem : SharedPuddleSystem
#region Spill #region Spill
/// <summary> /// <inheritdoc/>
/// First splashes reagent on reactive entities near the spilling entity, then spills the rest regularly to a public override bool TrySplashSpillAt(EntityUid uid,
/// puddle. This is intended for 'destructive' spills, like when entities are destroyed or thrown.
/// </summary>
public bool TrySplashSpillAt(EntityUid uid,
EntityCoordinates coordinates, EntityCoordinates coordinates,
Solution solution, Solution solution,
out EntityUid puddleUid, out EntityUid puddleUid,
@@ -600,11 +596,8 @@ public sealed partial class PuddleSystem : SharedPuddleSystem
return TrySpillAt(coordinates, solution, out puddleUid, sound); return TrySpillAt(coordinates, solution, out puddleUid, sound);
} }
/// <summary> /// <inheritdoc/>
/// Spills solution at the specified coordinates. public override bool TrySpillAt(EntityCoordinates coordinates, Solution solution, out EntityUid puddleUid, bool sound = true)
/// Will add to an existing puddle if present or create a new one if not.
/// </summary>
public bool TrySpillAt(EntityCoordinates coordinates, Solution solution, out EntityUid puddleUid, bool sound = true)
{ {
if (solution.Volume == 0) if (solution.Volume == 0)
{ {
@@ -622,10 +615,8 @@ public sealed partial class PuddleSystem : SharedPuddleSystem
return TrySpillAt(_map.GetTileRef(gridUid.Value, mapGrid, coordinates), solution, out puddleUid, sound); return TrySpillAt(_map.GetTileRef(gridUid.Value, mapGrid, coordinates), solution, out puddleUid, sound);
} }
/// <summary> /// <inheritdoc/>
/// <see cref="TrySpillAt(Robust.Shared.Map.EntityCoordinates,Content.Shared.Chemistry.Components.Solution,out Robust.Shared.GameObjects.EntityUid,bool)"/> public override bool TrySpillAt(EntityUid uid, Solution solution, out EntityUid puddleUid, bool sound = true,
/// </summary>
public bool TrySpillAt(EntityUid uid, Solution solution, out EntityUid puddleUid, bool sound = true,
TransformComponent? transformComponent = null) TransformComponent? transformComponent = null)
{ {
if (!Resolve(uid, ref transformComponent, false)) if (!Resolve(uid, ref transformComponent, false))
@@ -637,10 +628,8 @@ public sealed partial class PuddleSystem : SharedPuddleSystem
return TrySpillAt(transformComponent.Coordinates, solution, out puddleUid, sound: sound); return TrySpillAt(transformComponent.Coordinates, solution, out puddleUid, sound: sound);
} }
/// <summary> /// <inheritdoc/>
/// <see cref="TrySpillAt(Robust.Shared.Map.EntityCoordinates,Content.Shared.Chemistry.Components.Solution,out Robust.Shared.GameObjects.EntityUid,bool)"/> public override bool TrySpillAt(TileRef tileRef, Solution solution, out EntityUid puddleUid, bool sound = true,
/// </summary>
public bool TrySpillAt(TileRef tileRef, Solution solution, out EntityUid puddleUid, bool sound = true,
bool tileReact = true) bool tileReact = true)
{ {
if (solution.Volume <= 0) if (solution.Volume <= 0)

View File

@@ -0,0 +1,12 @@
using Robust.Shared.GameStates;
namespace Content.Shared.Fluids.Components;
/// <summary>
/// Blocks this entity's ability to spill solution containing entities via the verb menu.
/// </summary>
[RegisterComponent, NetworkedComponent]
public sealed partial class PreventSpillerComponent : Component
{
}

View File

@@ -2,6 +2,12 @@ using Content.Shared.FixedPoint;
namespace Content.Shared.Fluids.Components; namespace Content.Shared.Fluids.Components;
/// <summary>
/// Makes a solution contained in this entity spillable.
/// Spills can occur when a container with this component overflows,
/// is used to melee attack something, is equipped (see <see cref="SpillWorn"/>),
/// lands after being thrown, or has the Spill verb used.
/// </summary>
[RegisterComponent] [RegisterComponent]
public sealed partial class SpillableComponent : Component public sealed partial class SpillableComponent : Component
{ {

View File

@@ -1,14 +1,23 @@
using Content.Shared.Database;
using Content.Shared.DoAfter;
using Content.Shared.Examine; using Content.Shared.Examine;
using Content.Shared.FixedPoint;
using Content.Shared.Fluids.Components; using Content.Shared.Fluids.Components;
using Content.Shared.Nutrition.EntitySystems;
using Content.Shared.Spillable;
using Content.Shared.Verbs;
using Content.Shared.Weapons.Melee; using Content.Shared.Weapons.Melee;
namespace Content.Shared.Fluids; namespace Content.Shared.Fluids;
public abstract partial class SharedPuddleSystem public abstract partial class SharedPuddleSystem
{ {
[Dependency] protected readonly SharedOpenableSystem Openable = default!;
protected virtual void InitializeSpillable() protected virtual void InitializeSpillable()
{ {
SubscribeLocalEvent<SpillableComponent, ExaminedEvent>(OnExamined); SubscribeLocalEvent<SpillableComponent, ExaminedEvent>(OnExamined);
SubscribeLocalEvent<SpillableComponent, GetVerbsEvent<Verb>>(AddSpillVerb);
} }
private void OnExamined(Entity<SpillableComponent> entity, ref ExaminedEvent args) private void OnExamined(Entity<SpillableComponent> entity, ref ExaminedEvent args)
@@ -21,4 +30,55 @@ public abstract partial class SharedPuddleSystem
args.PushMarkup(Loc.GetString("spill-examine-spillable-weapon")); args.PushMarkup(Loc.GetString("spill-examine-spillable-weapon"));
} }
} }
private void AddSpillVerb(Entity<SpillableComponent> entity, ref GetVerbsEvent<Verb> args)
{
if (!args.CanAccess || !args.CanInteract)
return;
if (!_solutionContainerSystem.TryGetSolution(args.Target, entity.Comp.SolutionName, out var soln, out var solution))
return;
if (Openable.IsClosed(args.Target))
return;
if (solution.Volume == FixedPoint2.Zero)
return;
if (HasComp<PreventSpillerComponent>(args.User))
return;
Verb verb = new()
{
Text = Loc.GetString("spill-target-verb-get-data-text")
};
// TODO VERB ICONS spill icon? pouring out a glass/beaker?
if (entity.Comp.SpillDelay == null)
{
var target = args.Target;
verb.Act = () =>
{
var puddleSolution = _solutionContainerSystem.SplitSolution(soln.Value, solution.Volume);
TrySpillAt(Transform(target).Coordinates, puddleSolution, out _);
};
}
else
{
var user = args.User;
verb.Act = () =>
{
_doAfterSystem.TryStartDoAfter(new DoAfterArgs(EntityManager, user, entity.Comp.SpillDelay ?? 0, new SpillDoAfterEvent(), entity.Owner, target: entity.Owner)
{
BreakOnDamage = true,
BreakOnMove = true,
NeedHand = true,
});
};
}
verb.Impact = LogImpact.Medium; // dangerous reagent reaction are logged separately.
verb.DoContactInteraction = true;
args.Verbs.Add(verb);
}
} }

View File

@@ -1,12 +1,14 @@
using Content.Shared.Chemistry.Components; using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.EntitySystems; using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Chemistry.Reagent; using Content.Shared.Chemistry.Reagent;
using Content.Shared.DoAfter;
using Content.Shared.DragDrop; using Content.Shared.DragDrop;
using Content.Shared.Examine; using Content.Shared.Examine;
using Content.Shared.FixedPoint; using Content.Shared.FixedPoint;
using Content.Shared.Fluids.Components; using Content.Shared.Fluids.Components;
using Content.Shared.Movement.Events; using Content.Shared.Movement.Events;
using Content.Shared.StepTrigger.Components; using Content.Shared.StepTrigger.Components;
using Robust.Shared.Map;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
namespace Content.Shared.Fluids; namespace Content.Shared.Fluids;
@@ -15,6 +17,7 @@ public abstract partial class SharedPuddleSystem : EntitySystem
{ {
[Dependency] private readonly IPrototypeManager _prototypeManager = default!; [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly SharedSolutionContainerSystem _solutionContainerSystem = default!; [Dependency] private readonly SharedSolutionContainerSystem _solutionContainerSystem = default!;
[Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!;
/// <summary> /// <summary>
/// The lowest threshold to be considered for puddle sprite states as well as slipperiness of a puddle. /// The lowest threshold to be considered for puddle sprite states as well as slipperiness of a puddle.
@@ -106,4 +109,54 @@ public abstract partial class SharedPuddleSystem : EntitySystem
args.PushMarkup(Loc.GetString("puddle-component-examine-evaporating-no")); args.PushMarkup(Loc.GetString("puddle-component-examine-evaporating-no"));
} }
} }
#region Spill
// These methods are in Shared to make it easier to interact with PuddleSystem in Shared code.
// Note that they always fail when run on the client, not creating a puddle and returning false.
// Adding proper prediction to this system would require spawning temporary puddle entities on the
// client and replacing or merging them with the ones spawned by the server when the client goes to
// replicate those, and I am not enough of a wizard to attempt implementing that.
/// <summary>
/// First splashes reagent on reactive entities near the spilling entity, then spills the rest regularly to a
/// puddle. This is intended for 'destructive' spills, like when entities are destroyed or thrown.
/// </summary>
/// <remarks>
/// On the client, this will always set <paramref name="puddleUid"/> to <see cref="EntityUid.Invalid"> and return false.
/// </remarks>
public abstract bool TrySplashSpillAt(EntityUid uid,
EntityCoordinates coordinates,
Solution solution,
out EntityUid puddleUid,
bool sound = true,
EntityUid? user = null);
/// <summary>
/// Spills solution at the specified coordinates.
/// Will add to an existing puddle if present or create a new one if not.
/// </summary>
/// <remarks>
/// On the client, this will always set <paramref name="puddleUid"/> to <see cref="EntityUid.Invalid"> and return false.
/// </remarks>
public abstract bool TrySpillAt(EntityCoordinates coordinates, Solution solution, out EntityUid puddleUid, bool sound = true);
/// <summary>
/// <see cref="TrySpillAt(EntityCoordinates, Solution, out EntityUid, bool)"/>
/// </summary>
/// <remarks>
/// On the client, this will always set <paramref name="puddleUid"/> to <see cref="EntityUid.Invalid"> and return false.
/// </remarks>
public abstract bool TrySpillAt(EntityUid uid, Solution solution, out EntityUid puddleUid, bool sound = true,
TransformComponent? transformComponent = null);
/// <summary>
/// <see cref="TrySpillAt(EntityCoordinates, Solution, out EntityUid, bool)"/>
/// </summary>
/// <remarks>
/// On the client, this will always set <paramref name="puddleUid"/> to <see cref="EntityUid.Invalid"> and return false.
/// </remarks>
public abstract bool TrySpillAt(TileRef tileRef, Solution solution, out EntityUid puddleUid, bool sound = true,
bool tileReact = true);
#endregion Spill
} }