#nullable enable using Content.Shared.Chemistry; using Content.Shared.GameObjects.EntitySystems; using Robust.Shared.GameObjects; using Robust.Shared.GameObjects.Components.Appearance; using Robust.Shared.GameObjects.Systems; using Robust.Shared.IoC; using Robust.Shared.Localization; using Robust.Shared.Log; using Robust.Shared.Maths; using Robust.Shared.Prototypes; using Robust.Shared.Serialization; using Robust.Shared.Utility; using Robust.Shared.ViewVariables; using System; using System.Collections.Generic; namespace Content.Shared.GameObjects.Components.Chemistry { /// /// Holds a with a limited volume. /// public abstract class SharedSolutionContainerComponent : Component, IExamine, ISolutionInteractionsComponent { public override string Name => "SolutionContainer"; /// public sealed override uint? NetID => ContentNetIDs.SOLUTION; [ViewVariables] public Solution Solution { get; private set; } = new(); public IReadOnlyList ReagentList => Solution.Contents; [ViewVariables(VVAccess.ReadWrite)] public ReagentUnit MaxVolume { get; set; } [ViewVariables] public ReagentUnit CurrentVolume => Solution.TotalVolume; /// /// Volume needed to fill this container. /// [ViewVariables] public ReagentUnit EmptyVolume => MaxVolume - CurrentVolume; [ViewVariables] public virtual Color Color => Solution.Color; /// /// If reactions will be checked for when adding reagents to the container. /// [ViewVariables(VVAccess.ReadWrite)] public bool CanReact { get; set; } [ViewVariables(VVAccess.ReadWrite)] public SolutionContainerCaps Capabilities { get; set; } public bool CanExamineContents => Capabilities.HasCap(SolutionContainerCaps.CanExamine); public bool CanUseWithChemDispenser => Capabilities.HasCap(SolutionContainerCaps.FitsInDispenser); public bool CanInject => Capabilities.HasCap(SolutionContainerCaps.Injectable) || CanRefill; public bool CanDraw => Capabilities.HasCap(SolutionContainerCaps.Drawable) || CanDrain; public bool CanRefill => Capabilities.HasCap(SolutionContainerCaps.Refillable); public bool CanDrain => Capabilities.HasCap(SolutionContainerCaps.Drainable); public override void ExposeData(ObjectSerializer serializer) { base.ExposeData(serializer); serializer.DataField(this, x => x.CanReact, "canReact", true); serializer.DataField(this, x => x.MaxVolume, "maxVol", ReagentUnit.New(0)); serializer.DataField(this, x => x.Solution, "contents", new Solution()); serializer.DataField(this, x => x.Capabilities, "caps", SolutionContainerCaps.None); } public void RemoveAllSolution() { if (CurrentVolume == 0) return; Solution.RemoveAllSolution(); ChemicalsRemoved(); } /// /// Adds reagent of an Id to the container. /// /// The Id of the reagent to add. /// The amount of reagent to add. /// The amount of reagent sucesfully added. /// If all the reagent could be added. public bool TryAddReagent(string reagentId, ReagentUnit quantity, out ReagentUnit acceptedQuantity) { acceptedQuantity = EmptyVolume > quantity ? quantity : EmptyVolume; Solution.AddReagent(reagentId, acceptedQuantity); if (acceptedQuantity > 0) ChemicalsAdded(); return acceptedQuantity == quantity; } /// /// Removes reagent of an Id to the container. /// /// 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(string reagentId, ReagentUnit quantity) { if (!Solution.ContainsReagent(reagentId)) return false; Solution.RemoveReagent(reagentId, quantity); ChemicalsRemoved(); return true; } /// /// Removes part of the solution in the container. /// /// the volume of solution to remove. /// The solution that was removed. public Solution SplitSolution(ReagentUnit quantity) { var splitSol = Solution.SplitSolution(quantity); ChemicalsRemoved(); return splitSol; } /// /// Checks if a solution can fit into the container. /// /// The solution that is trying to be added. /// If the solution can be fully added. public bool CanAddSolution(Solution solution) { return solution.TotalVolume <= EmptyVolume; } /// /// Adds a solution to the container, if it can fully fit. /// /// The solution to try to add. /// If the solution could be added. public bool TryAddSolution(Solution solution) { if (!CanAddSolution(solution)) return false; Solution.AddSolution(solution); ChemicalsAdded(); return true; } private void ChemicalsAdded() { ProcessReactions(); SolutionChanged(); UpdateAppearance(); Dirty(); } private void ChemicalsRemoved() { SolutionChanged(); UpdateAppearance(); Dirty(); } private void SolutionChanged() { EntitySystem.Get().HandleSolutionChange(Owner); } private void ProcessReactions() { if (!CanReact) return; EntitySystem.Get() .FullyReactSolution(Solution, Owner, MaxVolume); } void IExamine.Examine(FormattedMessage message, bool inDetailsRange) { if (!CanExamineContents) return; var prototypeManager = IoCManager.Resolve(); if (ReagentList.Count == 0) { message.AddText(Loc.GetString("Contains no chemicals.")); return; } var primaryReagent = Solution.GetPrimaryReagentId(); if (!prototypeManager.TryIndex(primaryReagent, out ReagentPrototype proto)) { Logger.Error($"{nameof(SharedSolutionContainerComponent)} could not find the prototype associated with {primaryReagent}."); return; } var colorHex = Color.ToHexNoAlpha(); //TODO: If the chem has a dark color, the examine text becomes black on a black background, which is unreadable. var messageString = "It contains a [color={0}]{1}[/color] " + (ReagentList.Count == 1 ? "chemical." : "mixture of chemicals."); message.AddMarkup(Loc.GetString(messageString, colorHex, Loc.GetString(proto.PhysicalDescription))); } ReagentUnit ISolutionInteractionsComponent.RefillSpaceAvailable => EmptyVolume; ReagentUnit ISolutionInteractionsComponent.InjectSpaceAvailable => EmptyVolume; ReagentUnit ISolutionInteractionsComponent.DrawAvailable => CurrentVolume; ReagentUnit ISolutionInteractionsComponent.DrainAvailable => CurrentVolume; void ISolutionInteractionsComponent.Refill(Solution solution) { if (!CanRefill) return; TryAddSolution(solution); } void ISolutionInteractionsComponent.Inject(Solution solution) { if (!CanInject) return; TryAddSolution(solution); } Solution ISolutionInteractionsComponent.Draw(ReagentUnit amount) { if (!CanDraw) return new Solution(); return SplitSolution(amount); } Solution ISolutionInteractionsComponent.Drain(ReagentUnit amount) { if (!CanDrain) return new Solution(); return SplitSolution(amount); } private void UpdateAppearance() { if (Owner.Deleted || !Owner.TryGetComponent(out var appearance)) return; appearance.SetData(SolutionContainerVisuals.VisualState, GetVisualState()); } private SolutionContainerVisualState GetVisualState() { var filledVolumeFraction = CurrentVolume.Float() / MaxVolume.Float(); return new SolutionContainerVisualState(Color, filledVolumeFraction); } public override ComponentState GetComponentState() { return new SolutionContainerComponentState(Solution); } public override void HandleComponentState(ComponentState? curState, ComponentState? nextState) { if (curState is not SolutionContainerComponentState containerState) return; Solution = containerState.Solution; } } [Serializable, NetSerializable] public enum SolutionContainerVisuals : byte { VisualState } [Serializable, NetSerializable] public class SolutionContainerVisualState { public readonly Color Color; /// /// Represents how full the container is, as a fraction equivalent to /. /// public readonly byte FilledVolumeFraction; /// The fraction of the container's volume that is filled. public SolutionContainerVisualState(Color color, float filledVolumeFraction) { Color = color; FilledVolumeFraction = (byte) (byte.MaxValue * filledVolumeFraction); } } [Serializable, NetSerializable] public class SolutionContainerComponentState : ComponentState { public readonly Solution Solution; public SolutionContainerComponentState(Solution solution) : base(ContentNetIDs.SOLUTION) { Solution = solution; } } }