Raise SolutionChangedEvent and SolutionContainerChangedEvent when handling SolutionComponent states (#42814)

raise event on client + comp access
This commit is contained in:
slarticodefast 2026-02-27 22:27:56 +01:00 • committed by GitHub
parent 2484ec7f36
commit 210fae0ffe
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 105 additions and 22 deletions

View file

@ -50,7 +50,7 @@ namespace Content.IntegrationTests.Tests.Chemistry
beaker = entityManager.SpawnEntity("TestSolutionContainer", coordinates);
Assert.That(solutionContainerSystem
.TryGetSolution(beaker, "beaker", out solutionEnt, out solution));
solutionEnt.Value.Comp.Solution.CanReact = false;
solutionContainerSystem.SetCanReact(solutionEnt.Value, false);
foreach (var (id, reactant) in reactionPrototype.Reactants)
{
#pragma warning disable NUnit2045
@ -84,9 +84,8 @@ namespace Content.IntegrationTests.Tests.Chemistry
}
//Now safe set the temperature and mix the reagents
solutionEnt.Value.Comp.Solution.CanReact = true;
solutionContainerSystem.SetTemperature(solutionEnt.Value, reactionPrototype.MinimumTemperature);
solutionContainerSystem.UpdateChemicals(solutionEnt.Value);
solutionContainerSystem.SetCanReact(solutionEnt.Value, true);
if (reactionPrototype.MixingCategories != null)
{

View file

@ -188,6 +188,11 @@ namespace Content.Shared.Chemistry.Components
return new Solution(this);
}
public override string ToString()
{
return string.Join("; ", Contents);
}
[AssertionMethod]
public void ValidateSolution()
{

View file

@ -1,6 +1,8 @@
using Content.Shared.Chemistry.Components.SolutionManager;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Materials;
using Robust.Shared.GameStates;
using Robust.Shared.Serialization;
namespace Content.Shared.Chemistry.Components;
@ -11,12 +13,20 @@ namespace Content.Shared.Chemistry.Components;
/// <remarks>
/// <para>Once reagents and materials have been merged this component should be depricated in favor of using a combination of <see cref="PhysicalCompositionComponent"/> and <see cref="Content.Server.Temperature.Components.TemperatureComponent"/>. May require minor reworks to both.</para>
/// </remarks>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
[RegisterComponent, NetworkedComponent]
[Access(typeof(SharedSolutionContainerSystem))]
public sealed partial class SolutionComponent : Component
{
/// <summary>
/// <para>The reagents the entity is composed of and their temperature.</para>
/// </summary>
[DataField, AutoNetworkedField]
[DataField]
public Solution Solution = new();
}
[Serializable, NetSerializable]
public sealed class SolutionComponentState(Solution solution) : ComponentState
{
public Solution Solution = solution;
}

View file

@ -1,6 +1,7 @@
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.Reaction;
using Robust.Shared.Containers;
using Robust.Shared.Timing;
namespace Content.Shared.Chemistry.EntitySystems;
@ -9,6 +10,7 @@ public sealed class ReactiveContainerSystem : EntitySystem
[Dependency] private readonly SharedContainerSystem _containerSystem = default!;
[Dependency] private readonly ReactiveSystem _reactiveSystem = default!;
[Dependency] private readonly SharedSolutionContainerSystem _solutionContainerSystem = default!;
[Dependency] private readonly IGameTiming _timing = default!;
public override void Initialize()
{
@ -34,6 +36,10 @@ public sealed class ReactiveContainerSystem : EntitySystem
private void OnSolutionChange(EntityUid uid, ReactiveContainerComponent comp, SolutionContainerChangedEvent args)
{
// The changes are already networked as part of the same game state.
if (_timing.ApplyingState)
return;
if (!_solutionContainerSystem.TryGetSolution(uid, comp.Solution, out _, out var solution))
return;
if (solution.Volume == 0)

View file

@ -5,6 +5,7 @@ using Content.Shared.FixedPoint;
using Content.Shared.Popups;
using Robust.Shared.Network;
using Robust.Shared.Random;
using Robust.Shared.Timing;
namespace Content.Shared.Chemistry.EntitySystems;
@ -16,6 +17,7 @@ public sealed class RehydratableSystem : EntitySystem
[Dependency] private readonly SharedSolutionContainerSystem _solutions = default!;
[Dependency] private readonly SharedTransformSystem _xform = default!;
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
[Dependency] private readonly IGameTiming _timing = default!;
public override void Initialize()
{
@ -26,6 +28,10 @@ public sealed class RehydratableSystem : EntitySystem
private void OnSolutionChange(Entity<RehydratableComponent> ent, ref SolutionContainerChangedEvent args)
{
// The changes are already networked as part of the same game state.
if (_timing.ApplyingState)
return;
var quantity = _solutions.GetTotalPrototypeQuantity(ent, ent.Comp.CatalystPrototype);
_adminLogger.Add(LogType.Action, LogImpact.Medium, $"{ToPrettyString(ent.Owner)} was hydrated, now contains a solution of: {SharedSolutionContainerSystem.ToPrettyString(args.Solution)}.");
if (quantity != FixedPoint2.Zero && quantity >= ent.Comp.CatalystMinimum)

View file

@ -8,19 +8,20 @@ namespace Content.Shared.Chemistry.EntitySystems;
#region Events
/// <summary>
/// This event alerts system that the solution was changed
/// Raised on the container of the solution entity when the contained solution is changed.
/// If you want to subscribe with the solution entity itself
/// then use <see cref="SolutionChangedEvent"/> instead.
/// </summary>
/// <remarks>
/// This is always raised on the client when handling the component state so that we can update UIs accordingly.
/// You might need an IGameTiming.ApplyingState guard to prevent mispredicts if the changes from your subscription are
/// networked with the same game state.
/// </remarks>
[ByRefEvent]
public record struct SolutionContainerChangedEvent
public record struct SolutionContainerChangedEvent(Solution Solution, string SolutionId)
{
public readonly Solution Solution;
public readonly string SolutionId;
public SolutionContainerChangedEvent(Solution solution, string solutionId)
{
SolutionId = solutionId;
Solution = solution;
}
public readonly Solution Solution = Solution;
public readonly string SolutionId = SolutionId;
}
/// <summary>
@ -89,10 +90,14 @@ public abstract partial class SharedSolutionContainerSystem
var (solutionId, solutionComp) = args.Solution;
var solution = solutionComp.Solution;
UpdateAppearance(entity.Comp.Container, (solutionId, solutionComp, entity.Comp));
var relayEvent = new SolutionContainerChangedEvent(solution, entity.Comp.ContainerName);
RaiseLocalEvent(entity.Comp.Container, ref relayEvent);
// The appearance changes are already networked as part of the same game state.
if (_timing.ApplyingState)
return;
UpdateAppearance(entity.Comp.Container, (solutionId, solutionComp, entity.Comp));
}
protected virtual void OnSolutionOverflow(Entity<ContainedSolutionComponent> entity, ref SolutionOverflowEvent args)

View file

@ -2,7 +2,6 @@ using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Text;
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.Components.SolutionManager;
using Content.Shared.Chemistry.Reaction;
@ -10,17 +9,17 @@ using Content.Shared.Chemistry.Reagent;
using Content.Shared.Containers;
using Content.Shared.Examine;
using Content.Shared.FixedPoint;
using Content.Shared.Hands.Components;
using Content.Shared.Hands.EntitySystems;
using Content.Shared.Localizations;
using Content.Shared.Nutrition.Components;
using Content.Shared.Nutrition.EntitySystems;
using Content.Shared.Verbs;
using JetBrains.Annotations;
using Robust.Shared.Containers;
using Robust.Shared.GameStates;
using Robust.Shared.Map;
using Robust.Shared.Network;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
using Dependency = Robust.Shared.IoC.DependencyAttribute;
@ -28,9 +27,15 @@ namespace Content.Shared.Chemistry.EntitySystems;
/// <summary>
/// The event raised whenever a solution entity is modified.
/// Raised on the solution entity itself.
/// If you want to subscribe with the entity containing the solution entity
/// then use <see cref="SolutionContainerChangedEvent"/> instead.
/// </summary>
/// <remarks>
/// Raised after chemcial reactions and <see cref="SolutionOverflowEvent"/> are handled.
/// This is always raised on the client when handling the component state so that we can update UIs accordingly.
/// You might need an IGameTiming.ApplyingState guard to prevent mispredicts if the changes from your subscription are
/// networked with the same game state.
/// </remarks>
/// <param name="Solution">The solution entity that has been modified.</param>
[ByRefEvent]
@ -73,6 +78,7 @@ public abstract partial class SharedSolutionContainerSystem : EntitySystem
[Dependency] protected readonly SharedContainerSystem ContainerSystem = default!;
[Dependency] protected readonly MetaDataSystem MetaDataSys = default!;
[Dependency] protected readonly INetManager NetManager = default!;
[Dependency] private readonly IGameTiming _timing = default!;
public override void Initialize()
{
@ -80,6 +86,8 @@ public abstract partial class SharedSolutionContainerSystem : EntitySystem
InitializeRelays();
SubscribeLocalEvent<SolutionComponent, ComponentGetState>(OnSolutionGetState);
SubscribeLocalEvent<SolutionComponent, ComponentHandleState>(OnSolutionHandleState);
SubscribeLocalEvent<SolutionComponent, ComponentInit>(OnComponentInit);
SubscribeLocalEvent<SolutionComponent, ComponentStartup>(OnSolutionStartup);
SubscribeLocalEvent<SolutionComponent, ComponentShutdown>(OnSolutionShutdown);
@ -95,6 +103,22 @@ public abstract partial class SharedSolutionContainerSystem : EntitySystem
}
}
private void OnSolutionGetState(Entity<SolutionComponent> ent, ref ComponentGetState args)
{
args.State = new SolutionComponentState(ent.Comp.Solution);
}
private void OnSolutionHandleState(Entity<SolutionComponent> ent, ref ComponentHandleState args)
{
if (args.Current is not SolutionComponentState cast)
return;
ent.Comp.Solution = cast.Solution.Clone();
// Always raise the event on the client so that we can update UIs accordingly.
var changedEv = new SolutionChangedEvent(ent);
RaiseLocalEvent(ent, ref changedEv);
}
/// <summary>
/// Attempts to resolve a solution associated with an entity.
@ -434,6 +458,15 @@ public abstract partial class SharedSolutionContainerSystem : EntitySystem
UpdateChemicals(soln);
}
/// <summary>
/// Sets whether or not the given solution entity can react and dirties it.
/// </summary>
public void SetCanReact(Entity<SolutionComponent> soln, bool canReact)
{
soln.Comp.Solution.CanReact = canReact;
UpdateChemicals(soln);
}
/// <summary>
/// Adds reagent of an Id to the container.
/// </summary>

View file

@ -13,7 +13,8 @@ public sealed partial class ReagentEntityConditionSystem : EntityConditionSystem
{
protected override void Condition(Entity<SolutionComponent> entity, ref EntityConditionEvent<ReagentCondition> args)
{
var quant = entity.Comp.Solution.GetTotalPrototypeQuantity(args.Condition.Reagent);
var soln = entity.Comp.Solution;
var quant = soln.GetTotalPrototypeQuantity(args.Condition.Reagent);
args.Result = quant >= args.Condition.Min && quant <= args.Condition.Max;
}

View file

@ -11,6 +11,7 @@ using Content.Shared.Weapons.Melee;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Map.Components;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
namespace Content.Shared.Fluids;
@ -29,6 +30,7 @@ public abstract class SharedAbsorbentSystem : EntitySystem
[Dependency] private readonly UseDelaySystem _useDelay = default!;
[Dependency] private readonly SharedMapSystem _mapSystem = default!;
[Dependency] private readonly SharedItemSystem _item = default!;
[Dependency] private readonly IGameTiming _timing = default!;
public override void Initialize()
{
@ -59,6 +61,10 @@ public abstract class SharedAbsorbentSystem : EntitySystem
private void OnAbsorbentSolutionChange(Entity<AbsorbentComponent> ent, ref SolutionContainerChangedEvent args)
{
// The changes are already networked as part of the same game state.
if (_timing.ApplyingState)
return;
if (!SolutionContainer.TryGetSolution(ent.Owner, ent.Comp.SolutionName, out _, out var solution))
return;

View file

@ -114,6 +114,10 @@ public abstract partial class SharedPuddleSystem : EntitySystem
private void OnSolutionUpdate(Entity<PuddleComponent> entity, ref SolutionContainerChangedEvent args)
{
// The changes are already networked as part of the same game state.
if (_timing.ApplyingState)
return;
if (args.SolutionId != entity.Comp.SolutionName)
return;

View file

@ -97,7 +97,7 @@ public sealed class FoodSequenceSystem : SharedFoodSequenceSystem
return;
_solutionContainer.RemoveAllSolution(resultSoln.Value); //Remove all YML reagents
resultSoln.Value.Comp.Solution.MaxVolume = startSoln.Value.Comp.Solution.MaxVolume;
_solutionContainer.SetCapacity(resultSoln.Value, startSoln.Value.Comp.Solution.MaxVolume);
_solutionContainer.TryAddSolution(resultSoln.Value, startSolution);
MergeFlavorProfiles(start, result);

View file

@ -163,6 +163,10 @@ public sealed partial class IngestionSystem : EntitySystem
private void OnSolutionContainerChanged(Entity<EdibleComponent> entity, ref SolutionContainerChangedEvent args)
{
// The changes are already networked as part of the same game state.
if (_timing.ApplyingState)
return;
UpdateAppearance(entity);
}

View file

@ -267,6 +267,10 @@ public sealed partial class PressurizedSolutionSystem : EntitySystem
private void OnSolutionUpdate(Entity<PressurizedSolutionComponent> entity, ref SolutionContainerChangedEvent args)
{
// The changes are already networked as part of the same game state.
if (_timing.ApplyingState)
return;
if (args.SolutionId != entity.Comp.Solution)
return;