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;