Files
tbd-station-14/Content.Shared/GameObjects/Components/Chemistry/SharedSolutionContainerComponent.cs
Pieter-Jan Briers c40ac26ced A big hecking chemistry-related refactor. (#3055)
* A big hecking chemistry-related refactor.

Changed SolutionContainerCaps. It now describes "stock" behavior for interacting with solutions that is pre-implemented by SolutionContainerComponent. As such things like syringes do not check it anymore (on themselves) to see "can we remove reagent from ourselves". That's assumed by it... being a syringe.

SolutionContainerCaps now has different flags more accurately describing possible reagent interaction behaviors.

ISolutionInteractionsComponent is the interface that describes the common behaviors like "what happens when injected with a syringe". This is implemented by SolutionContainerComponent but could be implemented by other classes. One notable example that drove me to making this interface was the /vg/station circuit imprinter which splits reagent poured in into its two reservoir beakers. Having this interface allows us to do this "proxying" behavior hack-free. (the hacks in /vg/ code were somewhat dirty...).

PourableComponent has been replaced SolutionTransferComponent. It now describes both give-and-take behavior for the common reagent containers. This is in line with /vg/'s /obj/item/weapon/reagent_containers architecture. "Taking" in this context is ONLY from reagent tanks like fuel tanks.

Oh, should I mention that fuel tanks and such have a proper component now? They do.

Because of this behavioral change, reagent tanks DO NOT have Pourable anymore. Removing from reagent tanks is now in the hands of the item used on them. Welders and fire extinguishers now have code for removing from them. This sounds bad at first but remember that all have quite unique behavior related to this: Welders cause explosions if lit and can ONLY be fueled at fuel tanks. Extinguishers can be filled at any tank, etc... The code for this is also simpler due to ISolutionInteractionsComponent now so...

IAfterInteract now works like IInteractUsing with the Priority levels and "return true to block further handlers" behavior. This was necessary to make extinguishers prioritize taking from tanks over spraying.

Explicitly coded interactions like welders refueling also means they refuse instantly to full now, which they didn't before. And it plays the sound. Etc...

Probably more stuff I'm forgetting.

* Review improvements.
2021-02-04 00:05:31 +11:00

315 lines
11 KiB
C#

#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
{
/// <summary>
/// Holds a <see cref="Solution"/> with a limited volume.
/// </summary>
public abstract class SharedSolutionContainerComponent : Component, IExamine, ISolutionInteractionsComponent
{
public override string Name => "SolutionContainer";
/// <inheritdoc />
public sealed override uint? NetID => ContentNetIDs.SOLUTION;
[ViewVariables]
public Solution Solution { get; private set; } = new();
public IReadOnlyList<Solution.ReagentQuantity> ReagentList => Solution.Contents;
[ViewVariables(VVAccess.ReadWrite)]
public ReagentUnit MaxVolume { get; set; }
[ViewVariables]
public ReagentUnit CurrentVolume => Solution.TotalVolume;
/// <summary>
/// Volume needed to fill this container.
/// </summary>
[ViewVariables]
public ReagentUnit EmptyVolume => MaxVolume - CurrentVolume;
[ViewVariables]
public virtual Color Color => Solution.Color;
/// <summary>
/// If reactions will be checked for when adding reagents to the container.
/// </summary>
[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();
}
/// <summary>
/// Adds reagent of an Id to the container.
/// </summary>
/// <param name="reagentId">The Id of the reagent to add.</param>
/// <param name="quantity">The amount of reagent to add.</param>
/// <param name="acceptedQuantity">The amount of reagent sucesfully added.</param>
/// <returns>If all the reagent could be added.</returns>
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;
}
/// <summary>
/// Removes reagent of an Id to the container.
/// </summary>
/// <param name="reagentId">The Id of the reagent to remove.</param>
/// <param name="quantity">The amount of reagent to remove.</param>
/// <returns>If the reagent to remove was found in the container.</returns>
public bool TryRemoveReagent(string reagentId, ReagentUnit quantity)
{
if (!Solution.ContainsReagent(reagentId))
return false;
Solution.RemoveReagent(reagentId, quantity);
ChemicalsRemoved();
return true;
}
/// <summary>
/// Removes part of the solution in the container.
/// </summary>
/// <param name="quantity">the volume of solution to remove.</param>
/// <returns>The solution that was removed.</returns>
public Solution SplitSolution(ReagentUnit quantity)
{
var splitSol = Solution.SplitSolution(quantity);
ChemicalsRemoved();
return splitSol;
}
/// <summary>
/// Checks if a solution can fit into the container.
/// </summary>
/// <param name="solution">The solution that is trying to be added.</param>
/// <returns>If the solution can be fully added.</returns>
public bool CanAddSolution(Solution solution)
{
return solution.TotalVolume <= EmptyVolume;
}
/// <summary>
/// Adds a solution to the container, if it can fully fit.
/// </summary>
/// <param name="solution">The solution to try to add.</param>
/// <returns>If the solution could be added.</returns>
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<ChemistrySystem>().HandleSolutionChange(Owner);
}
private void ProcessReactions()
{
if (!CanReact)
return;
EntitySystem.Get<SharedChemicalReactionSystem>()
.FullyReactSolution(Solution, Owner, MaxVolume);
}
void IExamine.Examine(FormattedMessage message, bool inDetailsRange)
{
if (!CanExamineContents)
return;
var prototypeManager = IoCManager.Resolve<IPrototypeManager>();
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<SharedAppearanceComponent>(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;
/// <summary>
/// Represents how full the container is, as a fraction equivalent to <see cref="FilledVolumeFraction"/>/<see cref="byte.MaxValue"/>.
/// </summary>
public readonly byte FilledVolumeFraction;
/// <param name="filledVolumeFraction">The fraction of the container's volume that is filled.</param>
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;
}
}
}