using System.Diagnostics.CodeAnalysis; using System.Linq; using Content.Server.Chemistry.Components.SolutionManager; using Content.Shared.Chemistry; using Content.Shared.Chemistry.Components; using Content.Shared.Chemistry.Reaction; using Content.Shared.Chemistry.Reagent; using Content.Shared.Examine; using Content.Shared.FixedPoint; using JetBrains.Annotations; using Robust.Shared.Prototypes; using Robust.Shared.Utility; namespace Content.Server.Chemistry.EntitySystems; /// /// This event alerts system that the solution was changed /// public sealed class SolutionChangedEvent : EntityEventArgs { public readonly Solution Solution; public SolutionChangedEvent(Solution solution) { Solution = solution; } } /// /// Part of Chemistry system deal with SolutionContainers /// [UsedImplicitly] public sealed partial class SolutionContainerSystem : EntitySystem { [Dependency] private readonly SharedChemicalReactionSystem _chemistrySystem = default!; [Dependency] private readonly SharedAppearanceSystem _appearance = default!; [Dependency] private readonly IPrototypeManager _prototypeManager = default!; public override void Initialize() { base.Initialize(); SubscribeLocalEvent(InitSolution); SubscribeLocalEvent(OnExamineSolution); } private void InitSolution(EntityUid uid, SolutionContainerManagerComponent component, ComponentInit args) { foreach (var (name, solutionHolder) in component.Solutions) { solutionHolder.Name = name; solutionHolder.ValidateSolution(); UpdateAppearance(uid, solutionHolder); } } private void OnExamineSolution(EntityUid uid, ExaminableSolutionComponent examinableComponent, ExaminedEvent args) { SolutionContainerManagerComponent? solutionsManager = null; if (!Resolve(args.Examined, ref solutionsManager) || !solutionsManager.Solutions.TryGetValue(examinableComponent.Solution, out var solutionHolder)) return; var primaryReagent = solutionHolder.GetPrimaryReagentId(); if (string.IsNullOrEmpty(primaryReagent)) { args.PushText(Loc.GetString("shared-solution-container-component-on-examine-empty-container")); return; } if (!_prototypeManager.TryIndex(primaryReagent, out ReagentPrototype? proto)) { Logger.Error( $"{nameof(Solution)} could not find the prototype associated with {primaryReagent}."); return; } var colorHex = solutionHolder.GetColor(_prototypeManager) .ToHexNoAlpha(); //TODO: If the chem has a dark color, the examine text becomes black on a black background, which is unreadable. var messageString = "shared-solution-container-component-on-examine-main-text"; args.PushMarkup(Loc.GetString(messageString, ("color", colorHex), ("wordedAmount", Loc.GetString(solutionHolder.Contents.Count == 1 ? "shared-solution-container-component-on-examine-worded-amount-one-reagent" : "shared-solution-container-component-on-examine-worded-amount-multiple-reagents")), ("desc", proto.LocalizedPhysicalDescription))); } public void UpdateAppearance(EntityUid uid, Solution solution, AppearanceComponent? appearanceComponent = null) { if (!EntityManager.EntityExists(uid) || !Resolve(uid, ref appearanceComponent, false)) return; _appearance.SetData(uid, SolutionContainerVisuals.FillFraction, solution.FillFraction, appearanceComponent); _appearance.SetData(uid, SolutionContainerVisuals.Color, solution.GetColor(_prototypeManager), appearanceComponent); if (solution.GetPrimaryReagentId() is { } reagent) { _appearance.SetData(uid, SolutionContainerVisuals.BaseOverride, reagent, appearanceComponent); } else { _appearance.SetData(uid, SolutionContainerVisuals.BaseOverride, string.Empty, appearanceComponent); } } /// /// Removes part of the solution in the container. /// /// /// /// the volume of solution to remove. /// The solution that was removed. public Solution SplitSolution(EntityUid targetUid, Solution solutionHolder, FixedPoint2 quantity) { var splitSol = solutionHolder.SplitSolution(quantity); UpdateChemicals(targetUid, solutionHolder); return splitSol; } public void UpdateChemicals(EntityUid uid, Solution solutionHolder, bool needsReactionsProcessing = false, ReactionMixerComponent? mixerComponent = null) { DebugTools.Assert(solutionHolder.Name != null && TryGetSolution(uid, solutionHolder.Name, out var tmp) && tmp == solutionHolder); // Process reactions if (needsReactionsProcessing && solutionHolder.CanReact) { _chemistrySystem.FullyReactSolution(solutionHolder, uid, solutionHolder.MaxVolume, mixerComponent); } UpdateAppearance(uid, solutionHolder); RaiseLocalEvent(uid, new SolutionChangedEvent(solutionHolder)); } public void RemoveAllSolution(EntityUid uid, Solution solutionHolder) { if (solutionHolder.Volume == 0) return; solutionHolder.RemoveAllSolution(); UpdateChemicals(uid, solutionHolder); } public void RemoveAllSolution(EntityUid uid, SolutionContainerManagerComponent? solutionContainerManager = null) { if (!Resolve(uid, ref solutionContainerManager)) return; foreach (var solution in solutionContainerManager.Solutions.Values) { RemoveAllSolution(uid, solution); } } /// /// Sets the capacity (maximum volume) of a solution to a new value. /// /// The entity containing the solution. /// The solution to set the capacity of. /// The value to set the capacity of the solution to. public void SetCapacity(EntityUid targetUid, Solution targetSolution, FixedPoint2 capacity) { if (targetSolution.MaxVolume == capacity) return; targetSolution.MaxVolume = capacity; if (capacity < targetSolution.Volume) targetSolution.RemoveSolution(targetSolution.Volume - capacity); UpdateChemicals(targetUid, targetSolution); } /// /// Adds reagent of an Id to the container. /// /// /// Container to which we are adding reagent /// The Id of the reagent to add. /// The amount of reagent to add. /// The amount of reagent successfully added. /// If all the reagent could be added. public bool TryAddReagent(EntityUid targetUid, Solution targetSolution, string reagentId, FixedPoint2 quantity, out FixedPoint2 acceptedQuantity, float? temperature = null) { acceptedQuantity = targetSolution.AvailableVolume > quantity ? quantity : targetSolution.AvailableVolume; if (acceptedQuantity <= 0) return quantity == 0; if (temperature == null) targetSolution.AddReagent(reagentId, acceptedQuantity); else targetSolution.AddReagent(_prototypeManager.Index(reagentId), acceptedQuantity, temperature.Value, _prototypeManager); UpdateChemicals(targetUid, targetSolution, true); return acceptedQuantity == quantity; } /// /// Removes reagent of an Id to the container. /// /// /// Solution container from which we are removing reagent /// The Id of the reagent to remove. /// The amount of reagent to remove. /// If the reagent to remove was found in the container. public bool TryRemoveReagent(EntityUid targetUid, Solution? container, string reagentId, FixedPoint2 quantity) { if (container == null || !container.ContainsReagent(reagentId)) return false; container.RemoveReagent(reagentId, quantity); UpdateChemicals(targetUid, container); return true; } /// /// Adds a solution to the container, if it can fully fit. /// /// entity holding targetSolution /// entity holding targetSolution /// solution being added /// If the solution could be added. public bool TryAddSolution(EntityUid targetUid, Solution? targetSolution, Solution addedSolution) { if (targetSolution == null || !targetSolution.CanAddSolution(addedSolution) || addedSolution.Volume == 0) return false; targetSolution.AddSolution(addedSolution, _prototypeManager); UpdateChemicals(targetUid, targetSolution, true); return true; } /// /// Moves some quantity of a solution from one solution to another. /// /// entity holding the source solution /// entity holding the target solution /// source solution /// target solution /// quantity of solution to move from source to target. If this is a negative number, the source & target roles are reversed. public bool TryTransferSolution(EntityUid sourceUid, EntityUid targetUid, Solution source, Solution target, FixedPoint2 quantity) { if (quantity < 0) return TryTransferSolution(targetUid, sourceUid, target, source, -quantity); quantity = FixedPoint2.Min(quantity, target.AvailableVolume, source.Volume); if (quantity == 0) return false; // TODO This should be made into a function that directly transfers reagents. currently this is quite // inefficient. target.AddSolution(source.SplitSolution(quantity), _prototypeManager); UpdateChemicals(sourceUid, source, false); UpdateChemicals(targetUid, target, true); return true; } /// /// Moves some quantity of a solution from one solution to another. /// /// entity holding the source solution /// entity holding the target solution /// source solution /// target solution /// quantity of solution to move from source to target. If this is a negative number, the source & target roles are reversed. public bool TryTransferSolution(EntityUid sourceUid, EntityUid targetUid, string source, string target, FixedPoint2 quantity) { if (!TryGetSolution(sourceUid, source, out var sourceSoln)) return false; if (!TryGetSolution(targetUid, target, out var targetSoln)) return false; return TryTransferSolution(sourceUid, targetUid, sourceSoln, targetSoln, quantity); } /// /// Adds a solution to the container, overflowing the rest. /// It will /// Unlike it will ignore size limits. /// /// entity holding targetSolution /// The container to which we try to add. /// solution being added /// After addition this much will be left in targetSolution. Should be less /// than targetSolution.TotalVolume /// Solution that exceeded overflowThreshold /// public bool TryMixAndOverflow(EntityUid targetUid, Solution targetSolution, Solution addedSolution, FixedPoint2 overflowThreshold, [NotNullWhen(true)] out Solution? overflowingSolution) { if (addedSolution.Volume == 0 || overflowThreshold > targetSolution.MaxVolume) { overflowingSolution = null; return false; } targetSolution.AddSolution(addedSolution, _prototypeManager); UpdateChemicals(targetUid, targetSolution, true); overflowingSolution = targetSolution.SplitSolution(FixedPoint2.Max(FixedPoint2.Zero, targetSolution.Volume - overflowThreshold)); return true; } public bool TryGetSolution(EntityUid uid, string name, [NotNullWhen(true)] out Solution? solution, SolutionContainerManagerComponent? solutionsMgr = null) { if (!Resolve(uid, ref solutionsMgr, false)) { solution = null; return false; } return solutionsMgr.Solutions.TryGetValue(name, out solution); } /// /// Will ensure a solution is added to given entity even if it's missing solutionContainerManager /// /// EntityUid to which to add solution /// name for the solution /// solution components used in resolves /// true if the solution already existed /// solution public Solution EnsureSolution(EntityUid uid, string name, out bool existed, SolutionContainerManagerComponent? solutionsMgr = null) { if (!Resolve(uid, ref solutionsMgr, false)) { solutionsMgr = EntityManager.EnsureComponent(uid); } if (!solutionsMgr.Solutions.TryGetValue(name, out var existing)) { var newSolution = new Solution() { Name = name }; solutionsMgr.Solutions.Add(name, newSolution); existed = false; return newSolution; } existed = true; return existing; } /// /// Will ensure a solution is added to given entity even if it's missing solutionContainerManager /// /// EntityUid to which to add solution /// name for the solution /// solution components used in resolves /// solution public Solution EnsureSolution(EntityUid uid, string name, SolutionContainerManagerComponent? solutionsMgr = null) => EnsureSolution(uid, name, out _, solutionsMgr); /// /// Will ensure a solution is added to given entity even if it's missing solutionContainerManager /// /// EntityUid to which to add solution /// name for the solution /// Ensures that the solution's maximum volume is larger than this value. /// solution components used in resolves /// solution public Solution EnsureSolution(EntityUid uid, string name, FixedPoint2 minVol, out bool existed, SolutionContainerManagerComponent? solutionsMgr = null) { if (!Resolve(uid, ref solutionsMgr, false)) { solutionsMgr = EntityManager.EnsureComponent(uid); } if (!solutionsMgr.Solutions.TryGetValue(name, out var existing)) { var newSolution = new Solution() { Name = name }; solutionsMgr.Solutions.Add(name, newSolution); existed = false; newSolution.MaxVolume = minVol; return newSolution; } existed = true; existing.MaxVolume = FixedPoint2.Max(existing.MaxVolume, minVol); return existing; } public Solution EnsureSolution(EntityUid uid, string name, IEnumerable reagents, bool setMaxVol = true, SolutionContainerManagerComponent? solutionsMgr = null) { if (!Resolve(uid, ref solutionsMgr, false)) solutionsMgr = EntityManager.EnsureComponent(uid); if (!solutionsMgr.Solutions.TryGetValue(name, out var existing)) { var newSolution = new Solution(reagents, setMaxVol); solutionsMgr.Solutions.Add(name, newSolution); return newSolution; } existing.SetContents(reagents, setMaxVol); return existing; } /// /// Removes an amount from all reagents in a solution, adding it to a new solution. /// /// The entity containing the solution. /// The solution to remove reagents from. /// The amount to remove from every reagent in the solution. /// A new solution containing every removed reagent from the original solution. public Solution RemoveEachReagent(EntityUid uid, Solution solution, FixedPoint2 quantity) { if (quantity <= 0) return new Solution(); var removedSolution = new Solution(); // RemoveReagent does a RemoveSwap, meaning we don't have to copy the list if we iterate it backwards. for (var i = solution.Contents.Count-1; i >= 0; i--) { var (reagentId, _) = solution.Contents[i]; var removedQuantity = solution.RemoveReagent(reagentId, quantity); if(removedQuantity > 0) removedSolution.AddReagent(reagentId, removedQuantity); } UpdateChemicals(uid, solution); return removedSolution; } public FixedPoint2 GetReagentQuantity(EntityUid owner, string reagentId) { var reagentQuantity = FixedPoint2.New(0); if (EntityManager.EntityExists(owner) && EntityManager.TryGetComponent(owner, out SolutionContainerManagerComponent? managerComponent)) { foreach (var solution in managerComponent.Solutions.Values) { reagentQuantity += solution.GetReagentQuantity(reagentId); } } return reagentQuantity; } public bool TryGetMixableSolution(EntityUid uid, [NotNullWhen(true)] out Solution? solution, SolutionContainerManagerComponent? solutionsMgr = null) { if (!Resolve(uid, ref solutionsMgr, false)) { solution = null; return false; } var getMixableSolutionAttempt = new GetMixableSolutionAttemptEvent(uid); RaiseLocalEvent(uid, ref getMixableSolutionAttempt); if(getMixableSolutionAttempt.MixedSolution != null) { solution = getMixableSolutionAttempt.MixedSolution; return true; } var tryGetSolution = solutionsMgr.Solutions.FirstOrNull(x => x.Value.CanMix); if (tryGetSolution.HasValue) { solution = tryGetSolution.Value.Value; return true; } solution = null; return false; } // Thermal energy and temperature management. #region Thermal Energy and Temperature /// /// Sets the temperature of a solution to a new value and then checks for reaction processing. /// /// The entity in which the solution is located. /// The solution to set the temperature of. /// The new value to set the temperature to. public void SetTemperature(EntityUid owner, Solution solution, float temperature) { if (temperature == solution.Temperature) return; solution.Temperature = temperature; UpdateChemicals(owner, solution, true); } /// /// Sets the thermal energy of a solution to a new value and then checks for reaction processing. /// /// The entity in which the solution is located. /// The solution to set the thermal energy of. /// The new value to set the thermal energy to. public void SetThermalEnergy(EntityUid owner, Solution solution, float thermalEnergy) { var heatCap = solution.GetHeatCapacity(_prototypeManager); solution.Temperature = heatCap == 0 ? 0 : thermalEnergy / heatCap; UpdateChemicals(owner, solution, true); } /// /// Adds some thermal energy to a solution and then checks for reaction processing. /// /// The entity in which the solution is located. /// The solution to set the thermal energy of. /// The new value to set the thermal energy to. public void AddThermalEnergy(EntityUid owner, Solution solution, float thermalEnergy) { if (thermalEnergy == 0.0f) return; var heatCap = solution.GetHeatCapacity(_prototypeManager); solution.Temperature += heatCap == 0 ? 0 : thermalEnergy / heatCap; UpdateChemicals(owner, solution, true); } #endregion Thermal Energy and Temperature }