Reagents & Solutions (#280)

* Added the ReagentPrototype class.

* Added the new Solution class.

* Added new shared SolutionComponent to the ECS system.

* Added some basic element and chemical reagent prototypes.

* Added a new Beaker item utilizing the SolutionComponent. This is a testing/debug entity, and should be removed or changed soon.

* Added filters for code coverage.

* Nightly work.

* Added the server SolutionComponent class.

* Added a bucket.
Verbs set up for solution interaction.

* Adds water tank entity to the game.

* Added a full water tank entity.
Solutions are properly serialized.
Solution can be poured between two containers.

* Solution class can now be enumerated.
SolutionComponent now calculates the color of the solution.

* Minor Cleanup.
This commit is contained in:
Acruid
2019-07-31 05:10:06 -07:00
committed by Pieter-Jan Briers
parent 41b72d5aa2
commit 2ea8bbf4eb
22 changed files with 1055 additions and 2 deletions

View File

@@ -0,0 +1,25 @@
using Robust.Shared.Maths;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
using YamlDotNet.RepresentationModel;
namespace Content.Shared.Chemistry
{
[Prototype("reagent")]
public class ReagentPrototype : IPrototype, IIndexedPrototype
{
public string ID { get; private set; }
public string Name { get; private set; }
public string Description { get; private set; }
public Color SubstanceColor { get; private set; }
public void LoadFrom(YamlMappingNode mapping)
{
ID = mapping.GetNode("id").AsString();
Name = mapping.GetNode("name").ToString();
Description = mapping.GetNode("desc").ToString();
SubstanceColor = mapping.TryGetNode("color", out var colorNode) ? colorNode.AsHexColor(Color.White) : Color.White;
}
}
}

View File

@@ -0,0 +1,273 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using Robust.Shared.Interfaces.Serialization;
using Robust.Shared.Serialization;
using Robust.Shared.Utility;
using Robust.Shared.ViewVariables;
namespace Content.Shared.Chemistry
{
/// <summary>
/// A solution of reagents.
/// </summary>
public class Solution : IExposeData, IEnumerable<Solution.ReagentQuantity>
{
// Most objects on the station hold only 1 or 2 reagents
[ViewVariables]
private List<ReagentQuantity> _contents = new List<ReagentQuantity>(2);
/// <summary>
/// The calculated total volume of all reagents in the solution (ex. Total volume of liquid in beaker).
/// </summary>
[ViewVariables]
public int TotalVolume { get; private set; }
/// <summary>
/// Constructs an empty solution (ex. an empty beaker).
/// </summary>
public Solution() { }
/// <summary>
/// Constructs a solution containing 100% of a reagent (ex. A beaker of pure water).
/// </summary>
/// <param name="reagentId">The prototype ID of the reagent to add.</param>
/// <param name="quantity">The quantity in milli-units.</param>
public Solution(string reagentId, int quantity)
{
AddReagent(reagentId, quantity);
}
/// <inheritdoc />
public void ExposeData(ObjectSerializer serializer)
{
serializer.DataField(ref _contents, "reagents", new List<ReagentQuantity>());
if (serializer.Reading)
{
TotalVolume = 0;
foreach (var reagent in _contents)
{
TotalVolume += reagent.Quantity;
}
}
}
/// <summary>
/// Adds a given quantity of a reagent directly into the solution.
/// </summary>
/// <param name="reagentId">The prototype ID of the reagent to add.</param>
/// <param name="quantity">The quantity in milli-units.</param>
public void AddReagent(string reagentId, int quantity)
{
if(quantity <= 0)
return;
for (var i = 0; i < _contents.Count; i++)
{
var reagent = _contents[i];
if (reagent.ReagentId != reagentId)
continue;
_contents[i] = new ReagentQuantity(reagentId, reagent.Quantity + quantity);
TotalVolume += quantity;
return;
}
_contents.Add(new ReagentQuantity(reagentId, quantity));
TotalVolume += quantity;
}
/// <summary>
/// Returns the amount of a single reagent inside the solution.
/// </summary>
/// <param name="reagentId">The prototype ID of the reagent to add.</param>
/// <returns>The quantity in milli-units.</returns>
public int GetReagentQuantity(string reagentId)
{
for (var i = 0; i < _contents.Count; i++)
{
if (_contents[i].ReagentId == reagentId)
return _contents[i].Quantity;
}
return 0;
}
public void RemoveReagent(string reagentId, int quantity)
{
if(quantity <= 0)
return;
for (var i = 0; i < _contents.Count; i++)
{
var reagent = _contents[i];
if(reagent.ReagentId != reagentId)
continue;
var curQuantity = reagent.Quantity;
var newQuantity = curQuantity - quantity;
if (newQuantity <= 0)
{
_contents.RemoveSwap(i);
TotalVolume -= curQuantity;
}
else
{
_contents[i] = new ReagentQuantity(reagentId, newQuantity);
TotalVolume -= quantity;
}
return;
}
}
public void RemoveSolution(int quantity)
{
if(quantity <=0)
return;
var ratio = (float)(TotalVolume - quantity) / TotalVolume;
if (ratio <= 0)
{
RemoveAllSolution();
return;
}
for (var i = 0; i < _contents.Count; i++)
{
var reagent = _contents[i];
var oldQuantity = reagent.Quantity;
// quantity taken is always a little greedy, so fractional quantities get rounded up to the nearest
// whole unit. This should prevent little bits of chemical remaining because of float rounding errors.
var newQuantity = (int)Math.Floor(oldQuantity * ratio);
_contents[i] = new ReagentQuantity(reagent.ReagentId, newQuantity);
}
TotalVolume = (int)Math.Floor(TotalVolume * ratio);
}
public void RemoveAllSolution()
{
_contents.Clear();
TotalVolume = 0;
}
public Solution SplitSolution(int quantity)
{
if (quantity <= 0)
return new Solution();
Solution newSolution;
if (quantity >= TotalVolume)
{
newSolution = Clone();
RemoveAllSolution();
return newSolution;
}
newSolution = new Solution();
var newTotalVolume = 0;
var ratio = (float)(TotalVolume - quantity) / TotalVolume;
for (var i = 0; i < _contents.Count; i++)
{
var reagent = _contents[i];
var newQuantity = (int)Math.Floor(reagent.Quantity * ratio);
var splitQuantity = reagent.Quantity - newQuantity;
_contents[i] = new ReagentQuantity(reagent.ReagentId, newQuantity);
newSolution._contents.Add(new ReagentQuantity(reagent.ReagentId, splitQuantity));
newTotalVolume += splitQuantity;
}
TotalVolume = (int)Math.Floor(TotalVolume * ratio);
newSolution.TotalVolume = newTotalVolume;
return newSolution;
}
public void AddSolution(Solution otherSolution)
{
for (var i = 0; i < otherSolution._contents.Count; i++)
{
var otherReagent = otherSolution._contents[i];
var found = false;
for (var j = 0; j < _contents.Count; j++)
{
var reagent = _contents[j];
if (reagent.ReagentId == otherReagent.ReagentId)
{
found = true;
_contents[j] = new ReagentQuantity(reagent.ReagentId, reagent.Quantity + otherReagent.Quantity);
break;
}
}
if (!found)
{
_contents.Add(new ReagentQuantity(otherReagent.ReagentId, otherReagent.Quantity));
}
}
TotalVolume += otherSolution.TotalVolume;
}
public Solution Clone()
{
var volume = 0;
var newSolution = new Solution();
for (var i = 0; i < _contents.Count; i++)
{
var reagent = _contents[i];
newSolution._contents.Add(reagent);
volume += reagent.Quantity;
}
newSolution.TotalVolume = volume;
return newSolution;
}
[Serializable, NetSerializable]
public readonly struct ReagentQuantity
{
public readonly string ReagentId;
public readonly int Quantity;
public ReagentQuantity(string reagentId, int quantity)
{
ReagentId = reagentId;
Quantity = quantity;
}
[ExcludeFromCodeCoverage]
public override string ToString()
{
return $"{ReagentId}:{Quantity}";
}
}
#region Enumeration
public IEnumerator<ReagentQuantity> GetEnumerator()
{
return _contents.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
#endregion
}
}

View File

@@ -0,0 +1,21 @@
using System;
using Robust.Shared.Serialization;
namespace Content.Shared.Chemistry
{
/// <summary>
/// These are the defined capabilities of a container of a solution.
/// </summary>
[Flags]
[Serializable, NetSerializable]
public enum SolutionCaps
{
None = 0,
PourIn = 1,
PourOut = 2,
Injector = 4,
Injectable = 8,
}
}

View File

@@ -0,0 +1,166 @@
using System;
using Content.Shared.Chemistry;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Maths;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
using Robust.Shared.ViewVariables;
namespace Content.Shared.GameObjects.Components.Chemistry
{
public class SolutionComponent : Component
{
#pragma warning disable 649
[Dependency] private readonly IPrototypeManager _prototypeManager;
#pragma warning restore 649
[ViewVariables]
private Solution _containedSolution;
private int _maxVolume;
private SolutionCaps _capabilities;
/// <summary>
/// The maximum volume of the container.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
public int MaxVolume
{
get => _maxVolume;
set => _maxVolume = value; // Note that the contents won't spill out if the capacity is reduced.
}
/// <summary>
/// The total volume of all the of the reagents in the container.
/// </summary>
[ViewVariables]
public int CurrentVolume => _containedSolution.TotalVolume;
/// <summary>
/// The current blended color of all the reagents in the container.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
public Color SubstanceColor { get; private set; }
/// <summary>
/// The current capabilities of this container (is the top open to pour? can I inject it into another object?).
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
public SolutionCaps Capabilities
{
get => _capabilities;
set => _capabilities = value;
}
/// <inheritdoc />
public override string Name => "Solution";
/// <inheritdoc />
public sealed override uint? NetID => ContentNetIDs.SOLUTION;
/// <inheritdoc />
public sealed override Type StateType => typeof(SolutionComponentState);
/// <inheritdoc />
public override void ExposeData(ObjectSerializer serializer)
{
base.ExposeData(serializer);
serializer.DataField(ref _maxVolume, "maxVol", 0);
serializer.DataField(ref _containedSolution, "contents", new Solution());
serializer.DataField(ref _capabilities, "caps", SolutionCaps.None);
}
public override void Startup()
{
base.Startup();
RecalculateColor();
}
public override void Shutdown()
{
base.Shutdown();
_containedSolution.RemoveAllSolution();
_containedSolution = new Solution();
}
public bool TryAddReagent(string reagentId, int quantity, out int acceptedQuantity)
{
throw new NotImplementedException();
}
public bool TryAddSolution(Solution solution)
{
if (solution.TotalVolume > (_maxVolume - _containedSolution.TotalVolume))
return false;
_containedSolution.AddSolution(solution);
RecalculateColor();
return true;
}
public Solution SplitSolution(int quantity)
{
return _containedSolution.SplitSolution(quantity);
}
private void RecalculateColor()
{
if(_containedSolution.TotalVolume == 0)
SubstanceColor = Color.White;
Color mixColor = default;
float runningTotalQuantity = 0;
foreach (var reagent in _containedSolution)
{
runningTotalQuantity += reagent.Quantity;
if(!_prototypeManager.TryIndex(reagent.ReagentId, out ReagentPrototype proto))
continue;
if (mixColor == default)
mixColor = proto.SubstanceColor;
mixColor = BlendRGB(mixColor, proto.SubstanceColor, reagent.Quantity / runningTotalQuantity);
}
}
private Color BlendRGB(Color rgb1, Color rgb2, float amount)
{
var r = (float)Math.Round(rgb1.R + (rgb2.R - rgb1.R) * amount, 1);
var g = (float)Math.Round(rgb1.G + (rgb2.G - rgb1.G) * amount, 1);
var b = (float)Math.Round(rgb1.B + (rgb2.B - rgb1.B) * amount, 1);
var alpha = (float)Math.Round(rgb1.A + (rgb2.A - rgb1.A) * amount, 1);
return new Color(r, g, b, alpha);
}
/// <inheritdoc />
public override ComponentState GetComponentState()
{
return new SolutionComponentState();
}
/// <inheritdoc />
public override void HandleComponentState(ComponentState curState, ComponentState nextState)
{
base.HandleComponentState(curState, nextState);
if(curState == null)
return;
var compState = (SolutionComponentState)curState;
//TODO: Make me work!
}
[Serializable, NetSerializable]
public class SolutionComponentState : ComponentState
{
public SolutionComponentState() : base(ContentNetIDs.SOLUTION) { }
}
}
}

View File

@@ -7,6 +7,7 @@
public const uint DESTRUCTIBLE = 1001;
public const uint TEMPERATURE = 1002;
public const uint HANDS = 1003;
public const uint SOLUTION = 1004;
public const uint STORAGE = 1005;
public const uint INVENTORY = 1006;
public const uint POWER_DEBUG_TOOL = 1007;

View File

@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using JetBrains.Annotations;
using Robust.Shared.Interfaces.GameObjects;
@@ -116,7 +117,7 @@ namespace Content.Shared.GameObjects
foreach (var component in entity.GetComponentInstances())
{
var type = component.GetType();
foreach (var nestedType in type.GetNestedTypes())
foreach (var nestedType in type.GetNestedTypes(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static))
{
if (!typeof(Verb).IsAssignableFrom(nestedType) || nestedType.IsAbstract)
{