diff --git a/Content.IntegrationTests/Tests/Chemistry/TryAllReactionsTest.cs b/Content.IntegrationTests/Tests/Chemistry/TryAllReactionsTest.cs
index 13d8bdc6d29..14f32e01617 100644
--- a/Content.IntegrationTests/Tests/Chemistry/TryAllReactionsTest.cs
+++ b/Content.IntegrationTests/Tests/Chemistry/TryAllReactionsTest.cs
@@ -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)
{
diff --git a/Content.Shared/Chemistry/Components/Solution.cs b/Content.Shared/Chemistry/Components/Solution.cs
index cd0314419a0..bea7a0a071a 100644
--- a/Content.Shared/Chemistry/Components/Solution.cs
+++ b/Content.Shared/Chemistry/Components/Solution.cs
@@ -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()
{
diff --git a/Content.Shared/Chemistry/Components/SolutionComponent.cs b/Content.Shared/Chemistry/Components/SolutionComponent.cs
index 687a545b00b..847f8f03c91 100644
--- a/Content.Shared/Chemistry/Components/SolutionComponent.cs
+++ b/Content.Shared/Chemistry/Components/SolutionComponent.cs
@@ -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;
///
/// Once reagents and materials have been merged this component should be depricated in favor of using a combination of and . May require minor reworks to both.
///
-[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
+[RegisterComponent, NetworkedComponent]
+[Access(typeof(SharedSolutionContainerSystem))]
public sealed partial class SolutionComponent : Component
{
///
/// The reagents the entity is composed of and their temperature.
///
- [DataField, AutoNetworkedField]
+ [DataField]
public Solution Solution = new();
}
+
+
+[Serializable, NetSerializable]
+public sealed class SolutionComponentState(Solution solution) : ComponentState
+{
+ public Solution Solution = solution;
+}
diff --git a/Content.Shared/Chemistry/EntitySystems/ReactiveContainerSystem.cs b/Content.Shared/Chemistry/EntitySystems/ReactiveContainerSystem.cs
index aa217c60ba3..2bfa1c9de38 100644
--- a/Content.Shared/Chemistry/EntitySystems/ReactiveContainerSystem.cs
+++ b/Content.Shared/Chemistry/EntitySystems/ReactiveContainerSystem.cs
@@ -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)
diff --git a/Content.Shared/Chemistry/EntitySystems/RehydratableSystem.cs b/Content.Shared/Chemistry/EntitySystems/RehydratableSystem.cs
index 0d7b53170ac..5a305593616 100644
--- a/Content.Shared/Chemistry/EntitySystems/RehydratableSystem.cs
+++ b/Content.Shared/Chemistry/EntitySystems/RehydratableSystem.cs
@@ -7,6 +7,7 @@ using Robust.Shared.Network;
using Robust.Shared.Random;
using Content.Shared.Examine;
using Content.Shared.Kitchen; // DeltaV - Improve animal cube interactions (31668 - Upstream)
+using Robust.Shared.Timing;
namespace Content.Shared.Chemistry.EntitySystems;
@@ -18,6 +19,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()
{
@@ -32,6 +34,10 @@ public sealed class RehydratableSystem : EntitySystem
private void OnSolutionChange(Entity 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)
diff --git a/Content.Shared/Chemistry/EntitySystems/SharedSolutionContainerSystem.Relays.cs b/Content.Shared/Chemistry/EntitySystems/SharedSolutionContainerSystem.Relays.cs
index e6e96fa80a3..11122415c5e 100644
--- a/Content.Shared/Chemistry/EntitySystems/SharedSolutionContainerSystem.Relays.cs
+++ b/Content.Shared/Chemistry/EntitySystems/SharedSolutionContainerSystem.Relays.cs
@@ -8,19 +8,20 @@ namespace Content.Shared.Chemistry.EntitySystems;
#region Events
///
-/// 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 instead.
///
+///
+/// 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.
+///
[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;
}
///
@@ -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 entity, ref SolutionOverflowEvent args)
diff --git a/Content.Shared/Chemistry/EntitySystems/SharedSolutionContainerSystem.cs b/Content.Shared/Chemistry/EntitySystems/SharedSolutionContainerSystem.cs
index 892e4e9cac1..e6e50b23b5f 100644
--- a/Content.Shared/Chemistry/EntitySystems/SharedSolutionContainerSystem.cs
+++ b/Content.Shared/Chemistry/EntitySystems/SharedSolutionContainerSystem.cs
@@ -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;
///
/// 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 instead.
///
///
/// Raised after chemcial reactions and 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.
///
/// The solution entity that has been modified.
[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(OnSolutionGetState);
+ SubscribeLocalEvent(OnSolutionHandleState);
SubscribeLocalEvent(OnComponentInit);
SubscribeLocalEvent(OnSolutionStartup);
SubscribeLocalEvent(OnSolutionShutdown);
@@ -95,6 +103,22 @@ public abstract partial class SharedSolutionContainerSystem : EntitySystem
}
}
+ private void OnSolutionGetState(Entity ent, ref ComponentGetState args)
+ {
+ args.State = new SolutionComponentState(ent.Comp.Solution);
+ }
+
+ private void OnSolutionHandleState(Entity 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);
+ }
///
/// Attempts to resolve a solution associated with an entity.
@@ -434,6 +458,15 @@ public abstract partial class SharedSolutionContainerSystem : EntitySystem
UpdateChemicals(soln);
}
+ ///
+ /// Sets whether or not the given solution entity can react and dirties it.
+ ///
+ public void SetCanReact(Entity soln, bool canReact)
+ {
+ soln.Comp.Solution.CanReact = canReact;
+ UpdateChemicals(soln);
+ }
+
///
/// Adds reagent of an Id to the container.
///
diff --git a/Content.Shared/EntityConditions/Conditions/ReagentEntityConditionSystem.cs b/Content.Shared/EntityConditions/Conditions/ReagentEntityConditionSystem.cs
index c1da05ae30f..c721c2fc9d3 100644
--- a/Content.Shared/EntityConditions/Conditions/ReagentEntityConditionSystem.cs
+++ b/Content.Shared/EntityConditions/Conditions/ReagentEntityConditionSystem.cs
@@ -13,7 +13,8 @@ public sealed partial class ReagentEntityConditionSystem : EntityConditionSystem
{
protected override void Condition(Entity entity, ref EntityConditionEvent 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;
}
diff --git a/Content.Shared/Fluids/SharedAbsorbentSystem.cs b/Content.Shared/Fluids/SharedAbsorbentSystem.cs
index 77f40aeee64..62c59b8227b 100644
--- a/Content.Shared/Fluids/SharedAbsorbentSystem.cs
+++ b/Content.Shared/Fluids/SharedAbsorbentSystem.cs
@@ -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 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;
diff --git a/Content.Shared/Fluids/SharedPuddleSystem.cs b/Content.Shared/Fluids/SharedPuddleSystem.cs
index 8f87734717d..e676eaf68c2 100644
--- a/Content.Shared/Fluids/SharedPuddleSystem.cs
+++ b/Content.Shared/Fluids/SharedPuddleSystem.cs
@@ -114,6 +114,10 @@ public abstract partial class SharedPuddleSystem : EntitySystem
private void OnSolutionUpdate(Entity 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;
diff --git a/Content.Shared/Nutrition/EntitySystems/FoodSequenceSystem.cs b/Content.Shared/Nutrition/EntitySystems/FoodSequenceSystem.cs
index 697845b231f..2acc895b7d2 100644
--- a/Content.Shared/Nutrition/EntitySystems/FoodSequenceSystem.cs
+++ b/Content.Shared/Nutrition/EntitySystems/FoodSequenceSystem.cs
@@ -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);
diff --git a/Content.Shared/Nutrition/EntitySystems/IngestionSystem.cs b/Content.Shared/Nutrition/EntitySystems/IngestionSystem.cs
index eac3457478d..7733aa2cb7a 100644
--- a/Content.Shared/Nutrition/EntitySystems/IngestionSystem.cs
+++ b/Content.Shared/Nutrition/EntitySystems/IngestionSystem.cs
@@ -163,6 +163,10 @@ public sealed partial class IngestionSystem : EntitySystem
private void OnSolutionContainerChanged(Entity entity, ref SolutionContainerChangedEvent args)
{
+ // The changes are already networked as part of the same game state.
+ if (_timing.ApplyingState)
+ return;
+
UpdateAppearance(entity);
}
diff --git a/Content.Shared/Nutrition/EntitySystems/PressurizedSolutionSystem.cs b/Content.Shared/Nutrition/EntitySystems/PressurizedSolutionSystem.cs
index 9a0e3e79fa6..1ddb9965b75 100644
--- a/Content.Shared/Nutrition/EntitySystems/PressurizedSolutionSystem.cs
+++ b/Content.Shared/Nutrition/EntitySystems/PressurizedSolutionSystem.cs
@@ -267,6 +267,10 @@ public sealed partial class PressurizedSolutionSystem : EntitySystem
private void OnSolutionUpdate(Entity 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;