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:
committed by
Pieter-Jan Briers
parent
41b72d5aa2
commit
2ea8bbf4eb
25
Content.Shared/Chemistry/ReagentPrototype.cs
Normal file
25
Content.Shared/Chemistry/ReagentPrototype.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
273
Content.Shared/Chemistry/Solution.cs
Normal file
273
Content.Shared/Chemistry/Solution.cs
Normal 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
|
||||
}
|
||||
}
|
||||
21
Content.Shared/Chemistry/SolutionCaps.cs
Normal file
21
Content.Shared/Chemistry/SolutionCaps.cs
Normal 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,
|
||||
}
|
||||
}
|
||||
@@ -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) { }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user