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; 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.Map; using Robust.Shared.Network; using Robust.Shared.Prototypes; using Robust.Shared.Utility; using Dependency = Robust.Shared.IoC.DependencyAttribute; namespace Content.Shared.Chemistry.EntitySystems; /// /// The event raised whenever a solution entity is modified. /// /// /// Raised after chemcial reactions and are handled. /// /// The solution entity that has been modified. [ByRefEvent] public readonly partial record struct SolutionChangedEvent(Entity Solution); /// /// The event raised whenever a solution entity is filled past its capacity. /// /// The solution entity that has been overfilled. /// The amount by which the solution entity has been overfilled. [ByRefEvent] public partial record struct SolutionOverflowEvent(Entity Solution, FixedPoint2 Overflow) { /// The solution entity that has been overfilled. public readonly Entity Solution = Solution; /// The amount by which the solution entity has been overfilled. public readonly FixedPoint2 Overflow = Overflow; /// Whether any of the event handlers for this event have handled overflow behaviour. public bool Handled = false; } [ByRefEvent] public partial record struct SolutionAccessAttemptEvent(string SolutionName) { public bool Cancelled; } /// /// Part of Chemistry system deal with SolutionContainers /// [UsedImplicitly] public abstract partial class SharedSolutionContainerSystem : EntitySystem { [Dependency] protected readonly IPrototypeManager PrototypeManager = default!; [Dependency] protected readonly ChemicalReactionSystem ChemicalReactionSystem = default!; [Dependency] protected readonly ExamineSystemShared ExamineSystem = default!; [Dependency] protected readonly OpenableSystem Openable = default!; [Dependency] protected readonly SharedAppearanceSystem AppearanceSystem = default!; [Dependency] protected readonly SharedHandsSystem Hands = default!; [Dependency] protected readonly SharedContainerSystem ContainerSystem = default!; [Dependency] protected readonly MetaDataSystem MetaDataSys = default!; [Dependency] protected readonly INetManager NetManager = default!; public override void Initialize() { base.Initialize(); InitializeRelays(); SubscribeLocalEvent(OnComponentInit); SubscribeLocalEvent(OnSolutionStartup); SubscribeLocalEvent(OnSolutionShutdown); SubscribeLocalEvent(OnContainerManagerInit); SubscribeLocalEvent(OnExamineSolution); SubscribeLocalEvent>(OnSolutionExaminableVerb); SubscribeLocalEvent(OnMapInit); if (NetManager.IsServer) { SubscribeLocalEvent(OnContainerManagerShutdown); SubscribeLocalEvent(OnContainedSolutionShutdown); } } /// /// Attempts to resolve a solution associated with an entity. /// /// The entity that holdes the container the solution entity is in. /// The name of the solution entities container. /// A reference to a solution entity to load the associated solution entity into. Will be unchanged if not null. /// Returns the solution state of the solution entity. /// Whether the solution was successfully resolved. [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool ResolveSolution(Entity container, string? name, [NotNullWhen(true)] ref Entity? entity, [NotNullWhen(true)] out Solution? solution) { if (!ResolveSolution(container, name, ref entity)) { solution = null; return false; } solution = entity.Value.Comp.Solution; return true; } /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool ResolveSolution(Entity container, string? name, [NotNullWhen(true)] ref Entity? entity) { if (entity is not null) { DebugTools.Assert(TryGetSolution(container, name, out var debugEnt) && debugEnt.Value.Owner == entity.Value.Owner); return true; } return TryGetSolution(container, name, out entity); } /// /// Attempts to fetch a solution entity associated with an entity. /// /// /// If the solution entity will be frequently accessed please use the equivalent method and cache the result. /// /// The entity the solution entity should be associated with. /// The name of the solution entity to fetch. /// Returns the solution entity that was fetched. /// Returns the solution state of the solution entity that was fetched. /// /// Should we print an error if the solution specified by name is missing /// public bool TryGetSolution( Entity container, string? name, [NotNullWhen(true)] out Entity? entity, [NotNullWhen(true)] out Solution? solution, bool errorOnMissing = false) { if (!TryGetSolution(container, name, out entity, errorOnMissing: errorOnMissing)) { solution = null; return false; } solution = entity.Value.Comp.Solution; return true; } /// public bool TryGetSolution( Entity container, string? name, [NotNullWhen(true)] out Entity? entity, bool errorOnMissing = false) { // use connected container instead of entity from arguments, if it exists. var ev = new GetConnectedContainerEvent(); RaiseLocalEvent(container, ref ev); if (ev.ContainerEntity.HasValue) container = ev.ContainerEntity.Value; EntityUid uid; if (name is null) uid = container; else if ( ContainerSystem.TryGetContainer(container, $"solution@{name}", out var solutionContainer) && solutionContainer is ContainerSlot solutionSlot && solutionSlot.ContainedEntity is { } containedSolution ) { var attemptEv = new SolutionAccessAttemptEvent(name); RaiseLocalEvent(container, ref attemptEv); if (attemptEv.Cancelled) { entity = null; return false; } uid = containedSolution; } else { entity = null; if (!errorOnMissing) return false; Log.Error($"{ToPrettyString(container)} does not have a solution with ID: {name}"); return false; } if (!TryComp(uid, out SolutionComponent? comp)) { entity = null; if (!errorOnMissing) return false; Log.Error($"{ToPrettyString(container)} does not have a solution with ID: {name}"); return false; } entity = (uid, comp); return true; } /// /// Version of TryGetSolution that doesn't take or return an entity. /// Used for prototypes and with old code parity. public bool TryGetSolution(SolutionContainerManagerComponent container, string name, [NotNullWhen(true)] out Solution? solution, bool errorOnMissing = false) { solution = null; if (container.Solutions != null) return container.Solutions.TryGetValue(name, out solution); if (!errorOnMissing) return false; Log.Error($"{container} does not have a solution with ID: {name}"); return false; } public IEnumerable<(string? Name, Entity Solution)> EnumerateSolutions(Entity container, bool includeSelf = true) { if (includeSelf && TryComp(container, out SolutionComponent? solutionComp)) yield return (null, (container.Owner, solutionComp)); if (!Resolve(container, ref container.Comp, logMissing: false)) yield break; foreach (var name in container.Comp.Containers) { var attemptEv = new SolutionAccessAttemptEvent(name); RaiseLocalEvent(container, ref attemptEv); if (attemptEv.Cancelled) continue; if (ContainerSystem.GetContainer(container, $"solution@{name}") is ContainerSlot slot && slot.ContainedEntity is { } solutionId) yield return (name, (solutionId, Comp(solutionId))); } } public IEnumerable<(string Name, Solution Solution)> EnumerateSolutions(SolutionContainerManagerComponent container) { if (container.Solutions is not { Count: > 0 } solutions) yield break; foreach (var (name, solution) in solutions) { yield return (name, solution); } } protected void UpdateAppearance(Entity container, Entity soln) { var (uid, appearanceComponent) = container; if (!HasComp(uid) || !Resolve(uid, ref appearanceComponent, logMissing: false)) return; var (_, comp, relation) = soln; var solution = comp.Solution; AppearanceSystem.SetData(uid, SolutionContainerVisuals.FillFraction, solution.FillFraction, appearanceComponent); AppearanceSystem.SetData(uid, SolutionContainerVisuals.Color, solution.GetColor(PrototypeManager), appearanceComponent); AppearanceSystem.SetData(uid, SolutionContainerVisuals.SolutionName, relation.ContainerName, appearanceComponent); if (solution.GetPrimaryReagentId() is { } reagent) AppearanceSystem.SetData(uid, SolutionContainerVisuals.BaseOverride, reagent.ToString(), appearanceComponent); } public FixedPoint2 GetTotalPrototypeQuantity(EntityUid owner, string reagentId) { var reagentQuantity = FixedPoint2.New(0); if (Exists(owner) && TryComp(owner, out SolutionContainerManagerComponent? managerComponent)) { foreach (var (_, soln) in EnumerateSolutions((owner, managerComponent))) { var solution = soln.Comp.Solution; reagentQuantity += solution.GetTotalPrototypeQuantity(reagentId); } } return reagentQuantity; } /// /// Dirties a solution entity that has been modified and prompts updates to chemical reactions and overflow state. /// Should be invoked whenever a solution entity is modified. /// /// /// 90% of this system is ensuring that this proc is invoked whenever a solution entity is changed. The other 10% is this proc. /// /// /// /// public void UpdateChemicals(Entity soln, bool needsReactionsProcessing = true, ReactionMixerComponent? mixerComponent = null) { Dirty(soln); var (uid, comp) = soln; var solution = comp.Solution; // Process reactions if (needsReactionsProcessing && solution.CanReact) ChemicalReactionSystem.FullyReactSolution(soln, mixerComponent); var overflow = solution.Volume - solution.MaxVolume; if (overflow > FixedPoint2.Zero) { var overflowEv = new SolutionOverflowEvent(soln, overflow); RaiseLocalEvent(uid, ref overflowEv); } UpdateAppearance((uid, comp, null)); var changedEv = new SolutionChangedEvent(soln); RaiseLocalEvent(uid, ref changedEv); } public void UpdateAppearance(Entity soln) { var (uid, comp, appearanceComponent) = soln; var solution = comp.Solution; if (!Exists(uid) || !Resolve(uid, ref appearanceComponent, false)) return; AppearanceSystem.SetData(uid, SolutionContainerVisuals.FillFraction, solution.FillFraction, appearanceComponent); AppearanceSystem.SetData(uid, SolutionContainerVisuals.Color, solution.GetColor(PrototypeManager), appearanceComponent); if (solution.GetPrimaryReagentId() is { } reagent) AppearanceSystem.SetData(uid, SolutionContainerVisuals.BaseOverride, reagent.ToString(), appearanceComponent); } /// /// Removes part of the solution in the container. /// /// /// /// the volume of solution to remove. /// The solution that was removed. public Solution SplitSolution(Entity soln, FixedPoint2 quantity) { var (uid, comp) = soln; var solution = comp.Solution; var splitSol = solution.SplitSolution(quantity); UpdateChemicals(soln); return splitSol; } public Solution SplitStackSolution(Entity soln, FixedPoint2 quantity, int stackCount) { var (uid, comp) = soln; var solution = comp.Solution; var splitSol = solution.SplitSolution(quantity / stackCount); solution.SplitSolution(quantity - splitSol.Volume); UpdateChemicals(soln); return splitSol; } /// /// Splits a solution without the specified reagent(s). /// [Obsolete("Use SplitSolutionWithout with params ProtoId")] public Solution SplitSolutionWithout(Entity soln, FixedPoint2 quantity, params string[] reagents) { var (uid, comp) = soln; var solution = comp.Solution; var splitSol = solution.SplitSolutionWithout(quantity, reagents); UpdateChemicals(soln); return splitSol; } /// /// Splits a solution without the specified reagent(s). /// public Solution SplitSolutionWithout(Entity soln, FixedPoint2 quantity, params ProtoId[] reagents) { var (uid, comp) = soln; var solution = comp.Solution; var splitSol = solution.SplitSolutionWithout(quantity, reagents); UpdateChemicals(soln); return splitSol; } public void RemoveAllSolution(Entity soln) { var (uid, comp) = soln; var solution = comp.Solution; if (solution.Volume == 0) return; solution.RemoveAllSolution(); UpdateChemicals(soln); } /// /// 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(Entity soln, FixedPoint2 capacity) { var (uid, comp) = soln; var solution = comp.Solution; if (solution.MaxVolume == capacity) return; solution.MaxVolume = capacity; UpdateChemicals(soln); } /// /// Adds reagent of an Id to the container. /// /// /// Container to which we are adding reagent /// The reagent to add. /// The amount of reagent successfully added. /// If all the reagent could be added. public bool TryAddReagent(Entity soln, ReagentQuantity reagentQuantity, out FixedPoint2 acceptedQuantity, float? temperature = null) { var (uid, comp) = soln; var solution = comp.Solution; acceptedQuantity = solution.AvailableVolume > reagentQuantity.Quantity ? reagentQuantity.Quantity : solution.AvailableVolume; if (acceptedQuantity <= 0) return reagentQuantity.Quantity == 0; if (temperature == null) { solution.AddReagent(reagentQuantity.Reagent, acceptedQuantity); } else { var proto = PrototypeManager.Index(reagentQuantity.Reagent.Prototype); solution.AddReagent(proto, acceptedQuantity, temperature.Value, PrototypeManager); } UpdateChemicals(soln); return acceptedQuantity == reagentQuantity.Quantity; } /// /// 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. /// If all the reagent could be added. [PublicAPI] public bool TryAddReagent(Entity soln, string prototype, FixedPoint2 quantity, float? temperature = null, List? data = null) => TryAddReagent(soln, new ReagentQuantity(prototype, quantity, data), out _, temperature); /// /// 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(Entity soln, string prototype, FixedPoint2 quantity, out FixedPoint2 acceptedQuantity, float? temperature = null, List? data = null) { var reagent = new ReagentQuantity(prototype, quantity, data); return TryAddReagent(soln, reagent, out acceptedQuantity, temperature); } /// /// Adds reagent of an Id to the container. /// /// /// Container to which we are adding reagent /// 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(Entity soln, ReagentId reagentId, FixedPoint2 quantity, out FixedPoint2 acceptedQuantity, float? temperature = null) { var quant = new ReagentQuantity(reagentId, quantity); return TryAddReagent(soln, quant, out acceptedQuantity, temperature); } /// /// Removes reagent from a container. /// /// /// Solution container from which we are removing reagent. /// The reagent to remove. /// The amount of reagent that was removed. public FixedPoint2 RemoveReagent(Entity soln, ReagentQuantity reagentQuantity) { var (uid, comp) = soln; var solution = comp.Solution; var quant = solution.RemoveReagent(reagentQuantity); if (quant <= FixedPoint2.Zero) return FixedPoint2.Zero; UpdateChemicals(soln); return quant; } /// /// Removes reagent from a container. /// /// /// Solution container from which we are removing reagent /// The Id of the reagent to remove. /// The amount of reagent to remove. /// The amount of reagent that was removed. public FixedPoint2 RemoveReagent(Entity soln, string prototype, FixedPoint2 quantity, List? data = null) { return RemoveReagent(soln, new ReagentQuantity(prototype, quantity, data)); } /// /// Removes reagent from a container. /// /// /// Solution container from which we are removing reagent /// The reagent to remove. /// The amount of reagent to remove. /// The amount of reagent that was removed. public FixedPoint2 RemoveReagent(Entity soln, ReagentId reagentId, FixedPoint2 quantity) { return RemoveReagent(soln, new ReagentQuantity(reagentId, quantity)); } /// /// 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(Entity soln, Solution source, FixedPoint2 quantity) { var (uid, comp) = soln; var solution = comp.Solution; if (quantity < 0) throw new InvalidOperationException("Quantity must be positive"); quantity = FixedPoint2.Min(quantity, solution.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. solution.AddSolution(source.SplitSolution(quantity), PrototypeManager); UpdateChemicals(soln); 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(Entity soln, Solution toAdd) { var (uid, comp) = soln; var solution = comp.Solution; if (toAdd.Volume == FixedPoint2.Zero) return true; if (toAdd.Volume > solution.AvailableVolume) return false; ForceAddSolution(soln, toAdd); return true; } /// /// Adds as much of a solution to a container as can fit. /// /// The entity containing /// The solution being added to. /// The solution being added to /// The quantity of the solution actually added. public FixedPoint2 AddSolution(Entity soln, Solution toAdd) { var (uid, comp) = soln; var solution = comp.Solution; if (toAdd.Volume == FixedPoint2.Zero) return FixedPoint2.Zero; var quantity = FixedPoint2.Max(FixedPoint2.Zero, FixedPoint2.Min(toAdd.Volume, solution.AvailableVolume)); if (quantity < toAdd.Volume) TryTransferSolution(soln, toAdd, quantity); else ForceAddSolution(soln, toAdd); return quantity; } /// /// Adds a solution to a container and updates the container. /// /// The entity containing /// The solution being added to. /// The solution being added to /// Whether any reagents were added to the solution. public bool ForceAddSolution(Entity soln, Solution toAdd) { var (uid, comp) = soln; var solution = comp.Solution; if (toAdd.Volume == FixedPoint2.Zero) return false; solution.AddSolution(toAdd, PrototypeManager); UpdateChemicals(soln); return true; } /// /// Adds a solution to the container, removing the overflow. /// Unlike it will ignore size limits. /// /// The entity containing /// The solution being added to. /// The solution being added to /// The combined volume above which the overflow will be returned. /// If the combined volume is below this an empty solution is returned. /// Solution that exceeded overflowThreshold /// Whether any reagents were added to . public bool TryMixAndOverflow(Entity soln, Solution toAdd, FixedPoint2 overflowThreshold, [MaybeNullWhen(false)] out Solution overflowingSolution) { var (uid, comp) = soln; var solution = comp.Solution; if (toAdd.Volume == 0 || overflowThreshold > solution.MaxVolume) { overflowingSolution = null; return false; } solution.AddSolution(toAdd, PrototypeManager); overflowingSolution = solution.SplitSolution(FixedPoint2.Max(FixedPoint2.Zero, solution.Volume - overflowThreshold)); UpdateChemicals(soln); return true; } /// /// 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(Entity soln, FixedPoint2 quantity) { var (uid, comp) = soln; var solution = comp.Solution; 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 (reagent, _) = solution.Contents[i]; var removedQuantity = solution.RemoveReagent(reagent, quantity); removedSolution.AddReagent(reagent, removedQuantity); } UpdateChemicals(soln); return removedSolution; } // 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(Entity soln, float temperature) { var (_, comp) = soln; var solution = comp.Solution; if (temperature == solution.Temperature) return; solution.Temperature = temperature; UpdateChemicals(soln); } /// /// 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(Entity soln, float thermalEnergy) { var (_, comp) = soln; var solution = comp.Solution; var heatCap = solution.GetHeatCapacity(PrototypeManager); solution.Temperature = heatCap == 0 ? 0 : thermalEnergy / heatCap; UpdateChemicals(soln); } /// /// 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(Entity soln, float thermalEnergy) { var (_, comp) = soln; var solution = comp.Solution; if (thermalEnergy == 0.0f) return; var heatCap = solution.GetHeatCapacity(PrototypeManager); solution.Temperature += heatCap == 0 ? 0 : thermalEnergy / heatCap; UpdateChemicals(soln); } #endregion Thermal Energy and Temperature #region Event Handlers private void OnComponentInit(Entity entity, ref ComponentInit args) { entity.Comp.Solution.ValidateSolution(); } private void OnSolutionStartup(Entity entity, ref ComponentStartup args) { UpdateChemicals(entity); } private void OnSolutionShutdown(Entity entity, ref ComponentShutdown args) { RemoveAllSolution(entity); } private void OnContainerManagerInit(Entity entity, ref ComponentInit args) { if (entity.Comp.Containers is not { Count: > 0 } containers) return; var containerManager = EnsureComp(entity); foreach (var name in containers) { // The actual solution entity should be directly held within the corresponding slot. ContainerSystem.EnsureContainer(entity.Owner, $"solution@{name}", containerManager); } } /// /// Shift click examine. /// private void OnExamineSolution(Entity entity, ref ExaminedEvent args) { if (!args.IsInDetailsRange || !CanSeeHiddenSolution(entity, args.Examiner) || !TryGetSolution(entity.Owner, entity.Comp.Solution, out _, out var solution)) return; using (args.PushGroup(nameof(ExaminableSolutionComponent))) { var primaryReagent = solution.GetPrimaryReagentId(); // If there's no primary reagent, assume the solution is empty and exit early if (string.IsNullOrEmpty(primaryReagent?.Prototype) || !PrototypeManager.Resolve(primaryReagent.Value.Prototype, out var primary)) { args.PushMarkup(Loc.GetString(entity.Comp.LocVolume, ("fillLevel", ExaminedVolumeDisplay.Empty))); return; } // Push amount of reagent args.PushMarkup(Loc.GetString(entity.Comp.LocVolume, ("fillLevel", ExaminedVolume(entity, solution, args.Examiner)), ("current", solution.Volume), ("max", solution.MaxVolume))); // Push the physical description of the primary reagent var colorHex = solution.GetColor(PrototypeManager) .ToHexNoAlpha(); //TODO: If the chem has a dark color, the examine text becomes black on a black background, which is unreadable. args.PushMarkup(Loc.GetString(entity.Comp.LocPhysicalQuality, ("color", colorHex), ("desc", primary.LocalizedPhysicalDescription), ("chemCount", solution.Contents.Count) )); // Push the recognizable reagents // Sort the reagents by amount, descending then alphabetically var sortedReagentPrototypes = solution.GetReagentPrototypes(PrototypeManager) .OrderByDescending(pair => pair.Value.Value) .ThenBy(pair => pair.Key.LocalizedName); // Collect recognizable reagents, like water or beer var recognized = new List(); foreach (var keyValuePair in sortedReagentPrototypes) { var proto = keyValuePair.Key; if (!proto.Recognizable) { continue; } recognized.Add(Loc.GetString("examinable-solution-recognized", ("color", proto.SubstanceColor.ToHexNoAlpha()), ("chemical", proto.LocalizedName))); } if (recognized.Count == 0) return; var msg = ContentLocalizationManager.FormatList(recognized); // Finally push the full message args.PushMarkup(Loc.GetString(entity.Comp.LocRecognizableReagents, ("recognizedString", msg))); } } /// An enum for how to display the solution. public ExaminedVolumeDisplay ExaminedVolume(Entity ent, Solution sol, EntityUid? examiner = null) { // Exact measurement if (ent.Comp.ExactVolume) return ExaminedVolumeDisplay.Exact; // General approximation return (int)PercentFull(sol) switch { 100 => ExaminedVolumeDisplay.Full, > 66 => ExaminedVolumeDisplay.MostlyFull, > 33 => HalfEmptyOrHalfFull(examiner), > 0 => ExaminedVolumeDisplay.MostlyEmpty, _ => ExaminedVolumeDisplay.Empty, }; } // Some spessmen see half full, some see half empty, but always the same one. private ExaminedVolumeDisplay HalfEmptyOrHalfFull(EntityUid? examiner = null) { // Optimistic when un-observed if (examiner == null) return ExaminedVolumeDisplay.HalfFull; var meta = MetaData(examiner.Value); if (meta.EntityName.Length > 0 && string.Compare(meta.EntityName.Substring(0, 1), "m", StringComparison.InvariantCultureIgnoreCase) > 0) return ExaminedVolumeDisplay.HalfFull; return ExaminedVolumeDisplay.HalfEmpty; } /// /// Full reagent scan, such as with chemical analysis goggles. /// private void OnSolutionExaminableVerb(Entity entity, ref GetVerbsEvent args) { if (!args.CanInteract || !args.CanAccess) return; var scanEvent = new SolutionScanEvent(); RaiseLocalEvent(args.User, scanEvent); if (!scanEvent.CanScan) { return; } if (!TryGetSolution(args.Target, entity.Comp.Solution, out _, out var solutionHolder)) { return; } if (!CanSeeHiddenSolution(entity, args.User)) return; var target = args.Target; var user = args.User; var verb = new ExamineVerb() { Act = () => { var markup = GetSolutionExamine(solutionHolder); ExamineSystem.SendExamineTooltip(user, target, markup, false, false); }, Text = Loc.GetString("scannable-solution-verb-text"), Message = Loc.GetString("scannable-solution-verb-message"), Category = VerbCategory.Examine, Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/drink.svg.192dpi.png")), }; args.Verbs.Add(verb); } private FormattedMessage GetSolutionExamine(Solution solution) { var msg = new FormattedMessage(); if (solution.Volume == 0) { msg.AddMarkupOrThrow(Loc.GetString("scannable-solution-empty-container")); return msg; } msg.AddMarkupOrThrow(Loc.GetString("scannable-solution-main-text")); var reagentPrototypes = solution.GetReagentPrototypes(PrototypeManager); // Sort the reagents by amount, descending then alphabetically var sortedReagentPrototypes = reagentPrototypes .OrderByDescending(pair => pair.Value.Value) .ThenBy(pair => pair.Key.LocalizedName); foreach (var (proto, quantity) in sortedReagentPrototypes) { msg.PushNewline(); msg.AddMarkupOrThrow(Loc.GetString("scannable-solution-chemical" , ("type", proto.LocalizedName) , ("color", proto.SubstanceColor.ToHexNoAlpha()) , ("amount", quantity))); } msg.PushNewline(); msg.AddMarkupOrThrow(Loc.GetString("scannable-solution-temperature", ("temperature", Math.Round(solution.Temperature)))); return msg; } /// /// Check if an examinable solution is hidden by something. /// private bool CanSeeHiddenSolution(Entity entity, EntityUid examiner) { // If not held-only then it's always visible. if (entity.Comp.HeldOnly && !Hands.IsHolding(examiner, entity, out _)) return false; if (!entity.Comp.ExaminableWhileClosed && Openable.IsClosed(entity.Owner, predicted: true)) return false; return true; } private void OnMapInit(Entity entity, ref MapInitEvent args) { EnsureAllSolutions(entity); } private void OnContainerManagerShutdown(Entity entity, ref ComponentShutdown args) { foreach (var name in entity.Comp.Containers) { if (ContainerSystem.TryGetContainer(entity, $"solution@{name}", out var solutionContainer)) ContainerSystem.ShutdownContainer(solutionContainer); } entity.Comp.Containers.Clear(); } private void OnContainedSolutionShutdown(Entity entity, ref ComponentShutdown args) { if (TryComp(entity.Comp.Container, out SolutionContainerManagerComponent? container)) { container.Containers.Remove(entity.Comp.ContainerName); Dirty(entity.Comp.Container, container); } if (ContainerSystem.TryGetContainer(entity, $"solution@{entity.Comp.ContainerName}", out var solutionContainer)) ContainerSystem.ShutdownContainer(solutionContainer); } #endregion Event Handlers public bool EnsureSolution( Entity entity, string name, [NotNullWhen(true)]out Solution? solution, FixedPoint2 maxVol = default) { return EnsureSolution(entity, name, maxVol, null, out _, out solution); } public bool EnsureSolution( Entity entity, string name, out bool existed, [NotNullWhen(true)]out Solution? solution, FixedPoint2 maxVol = default) { return EnsureSolution(entity, name, maxVol, null, out existed, out solution); } public bool EnsureSolution( Entity entity, string name, FixedPoint2 maxVol, Solution? prototype, out bool existed, [NotNullWhen(true)] out Solution? solution) { solution = null; existed = false; var (uid, meta) = entity; if (!Resolve(uid, ref meta)) throw new InvalidOperationException("Attempted to ensure solution on invalid entity."); var manager = EnsureComp(uid); if (meta.EntityLifeStage >= EntityLifeStage.MapInitialized) { EnsureSolutionEntity((uid, manager), name, out existed, out var solEnt, maxVol, prototype); solution = solEnt!.Value.Comp.Solution; return true; } solution = EnsureSolutionPrototype((uid, manager), name, maxVol, prototype, out existed); return true; } public void EnsureAllSolutions(Entity entity) { if (NetManager.IsClient) return; if (entity.Comp.Solutions is not { } prototypes) return; foreach (var (name, prototype) in prototypes) { EnsureSolutionEntity((entity.Owner, entity.Comp), name, out _, out _, prototype.MaxVolume, prototype); } entity.Comp.Solutions = null; Dirty(entity); } public bool EnsureSolutionEntity( Entity entity, string name, [NotNullWhen(true)] out Entity? solutionEntity, FixedPoint2 maxVol = default) => EnsureSolutionEntity(entity, name, out _, out solutionEntity, maxVol); public bool EnsureSolutionEntity( Entity entity, string name, out bool existed, [NotNullWhen(true)] out Entity? solutionEntity, FixedPoint2 maxVol = default, Solution? prototype = null ) { existed = true; solutionEntity = null; var (uid, container) = entity; var solutionSlot = ContainerSystem.EnsureContainer(uid, $"solution@{name}", out existed); if (!Resolve(uid, ref container, logMissing: false)) { existed = false; container = AddComp(uid); container.Containers.Add(name); if (NetManager.IsClient) return false; } else if (!existed) { container.Containers.Add(name); Dirty(uid, container); } var needsInit = false; SolutionComponent solutionComp; if (solutionSlot.ContainedEntity is not { } solutionId) { if (NetManager.IsClient) return false; prototype ??= new() { MaxVolume = maxVol }; prototype.Name = name; (solutionId, solutionComp, _) = SpawnSolutionUninitialized(solutionSlot, name, maxVol, prototype); existed = false; needsInit = true; Dirty(uid, container); } else { solutionComp = Comp(solutionId); DebugTools.Assert(TryComp(solutionId, out ContainedSolutionComponent? relation) && relation.Container == uid && relation.ContainerName == name); DebugTools.Assert(solutionComp.Solution.Name == name); var solution = solutionComp.Solution; solution.MaxVolume = FixedPoint2.Max(solution.MaxVolume, maxVol); // Depending on MapInitEvent order some systems can ensure solution empty solutions and conflict with the prototype solutions. // We want the reagents from the prototype to exist even if something else already created the solution. if (prototype is { Volume.Value: > 0 }) solution.AddSolution(prototype, PrototypeManager); Dirty(solutionId, solutionComp); } if (needsInit) EntityManager.InitializeAndStartEntity(solutionId, Transform(solutionId).MapID); solutionEntity = (solutionId, solutionComp); return true; } private Solution EnsureSolutionPrototype(Entity entity, string name, FixedPoint2 maxVol, Solution? prototype, out bool existed) { existed = true; var (uid, container) = entity; if (!Resolve(uid, ref container, logMissing: false)) { container = AddComp(uid); existed = false; } if (container.Solutions is null) container.Solutions = new(SolutionContainerManagerComponent.DefaultCapacity); if (!container.Solutions.TryGetValue(name, out var solution)) { solution = prototype ?? new() { Name = name, MaxVolume = maxVol }; container.Solutions.Add(name, solution); existed = false; } else solution.MaxVolume = FixedPoint2.Max(solution.MaxVolume, maxVol); Dirty(uid, container); return solution; } private Entity SpawnSolutionUninitialized(ContainerSlot container, string name, FixedPoint2 maxVol, Solution prototype) { var coords = new EntityCoordinates(container.Owner, Vector2.Zero); var uid = EntityManager.CreateEntityUninitialized(null, coords, null); var solution = new SolutionComponent() { Solution = prototype }; AddComp(uid, solution); var relation = new ContainedSolutionComponent() { Container = container.Owner, ContainerName = name }; AddComp(uid, relation); MetaDataSys.SetEntityName(uid, $"solution - {name}"); ContainerSystem.Insert(uid, container, force: true); return (uid, solution, relation); } public void AdjustDissolvedReagent( Entity dissolvedSolution, FixedPoint2 volume, ReagentId reagent, float concentrationChange) { if (concentrationChange == 0) return; var dissolvedSol = dissolvedSolution.Comp.Solution; var amtChange = GetReagentQuantityFromConcentration(dissolvedSolution, volume, MathF.Abs(concentrationChange)); if (concentrationChange > 0) { dissolvedSol.AddReagent(reagent, amtChange); } else { dissolvedSol.RemoveReagent(reagent,amtChange); } UpdateChemicals(dissolvedSolution); } public FixedPoint2 GetReagentQuantityFromConcentration(Entity dissolvedSolution, FixedPoint2 volume,float concentration) { var dissolvedSol = dissolvedSolution.Comp.Solution; if (volume == 0 || dissolvedSol.Volume == 0) return 0; return concentration * volume; } public float GetReagentConcentration(Entity dissolvedSolution, FixedPoint2 volume, ReagentId dissolvedReagent) { var dissolvedSol = dissolvedSolution.Comp.Solution; if (volume == 0 || dissolvedSol.Volume == 0 || !dissolvedSol.TryGetReagentQuantity(dissolvedReagent, out var dissolvedVol)) return 0; return (float)dissolvedVol / volume.Float(); } public FixedPoint2 ClampReagentAmountByConcentration( Entity dissolvedSolution, FixedPoint2 volume, ReagentId dissolvedReagent, FixedPoint2 dissolvedReagentAmount, float maxConcentration = 1f) { var dissolvedSol = dissolvedSolution.Comp.Solution; if (volume == 0 || dissolvedSol.Volume == 0 || !dissolvedSol.TryGetReagentQuantity(dissolvedReagent, out var dissolvedVol)) return 0; volume *= maxConcentration; dissolvedVol += dissolvedReagentAmount; var overflow = volume - dissolvedVol; if (overflow < 0) dissolvedReagentAmount += overflow; return dissolvedReagentAmount; } }