Bases of a Recipe System created, tags compponet, and Tempearue stuct with a temutrae delta.

This commit is contained in:
2026-05-04 00:02:08 -04:00
parent 4a7494ff6b
commit d620189e4e
8 changed files with 1481 additions and 13 deletions

View File

@@ -10,6 +10,7 @@
<!-- Required for some nuget packages to work -->
<!-- godotengine/godot/issues/42271#issuecomment-751423827 -->
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<!-- To show generated files -->
<!-- <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles> -->
<!--
@@ -35,31 +36,34 @@
<ItemGroup>
<!-- Production dependencies go here! -->
<PackageReference Include="Chickensoft.GameTools" Version="3.1.6" />
<PackageReference Include="SjkScripts" Version="1.0.1" />
<PackageReference Include="System.IO.Abstractions" Version="22.1.0" />
<PackageReference Include="Arch" Version="2.1.0" />
<PackageReference Include="Chickensoft.GameTools" Version="3.1.18" />
<PackageReference Include="NCalc.LambdaCompilation" Version="5.12.0" />
<PackageReference Include="NCalcSync" Version="5.12.0" />
<PackageReference Include="SjkScripts" Version="1.0.17" />
<PackageReference Include="System.IO.Abstractions" Version="22.1.1" />
<PackageReference Include="EnvironmentAbstractions" Version="5.0.0" />
<PackageReference Include="GodotSharp.SourceGenerators" Version="2.6.0" PrivateAssets="all" OutputItemType="analyzer" />
<PackageReference Include="Chickensoft.SaveFileBuilder" Version="1.3.54" />
<PackageReference Include="Chickensoft.AutoInject" Version="2.9.18" PrivateAssets="all" />
<PackageReference Include="Chickensoft.SaveFileBuilder" Version="1.3.66" />
<PackageReference Include="Chickensoft.AutoInject" Version="2.13.0" PrivateAssets="all" />
<PackageReference Include="Chickensoft.Collections" Version="3.1.4" />
<PackageReference Include="Chickensoft.GodotNodeInterfaces" Version="2.4.57" />
<PackageReference Include="Chickensoft.GodotNodeInterfaces" Version="3.0.12" />
<PackageReference Include="Chickensoft.Introspection" Version="3.0.2" />
<PackageReference Include="Chickensoft.Introspection.Generator" Version="3.0.2" PrivateAssets="all" OutputItemType="analyzer" />
<PackageReference Include="Chickensoft.Serialization" Version="3.1.0" />
<PackageReference Include="Chickensoft.Serialization.Godot" Version="0.8.46" />
<PackageReference Include="Chickensoft.Serialization.Godot" Version="0.8.64" />
<PackageReference Include="Chickensoft.LogicBlocks" Version="5.20.0" />
<PackageReference Include="Chickensoft.LogicBlocks.DiagramGenerator" Version="5.20.0" PrivateAssets="all" OutputItemType="analyzer" />
<PackageReference Include="Chickensoft.UMLGenerator" Version="1.1.0" />
<PackageReference Include="Chickensoft.Sync" Version="2.2.0" />
<PackageReference Include="Chickensoft.UMLGenerator" Version="1.3.1" />
<PackageReference Include="Chickensoft.Sync" Version="2.3.0" />
</ItemGroup>
<ItemGroup Condition="'$(RunTests)' == 'true'">
<!-- Test dependencies go here! -->
<!-- Dependencies added here will not be included in release builds. -->
<PackageReference Include="Chickensoft.GoDotTest" Version="2.0.27" />
<PackageReference Include="Chickensoft.GoDotTest" Version="2.0.33" />
<!-- Used to drive test scenes when testing visual code -->
<PackageReference Include="Chickensoft.GodotTestDriver" Version="3.1.56" />
<PackageReference Include="Chickensoft.GodotTestDriver" Version="3.1.68" />
<!-- Bring your own assertion library for tests! -->
<!-- We're using Shouldly for this example, but you can use anything. -->
<PackageReference Include="Shouldly" Version="4.3.0" />

View File

@@ -7,8 +7,8 @@ public partial class Game : Control
public Button TestButton { get; private set; } = default!;
public int ButtonPresses { get; private set; }
public override void _Ready()
=> TestButton = GetNode<Button>("%TestButton");
public override void _Ready()=> new Test().TestECS();
// => TestButton = GetNode<Button>("%TestButton");
public void OnTestButtonPressed() => ButtonPresses++;

View File

@@ -0,0 +1,289 @@
namespace FoodFactory.Items;
using System;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Numerics;
using System.Threading;
public readonly struct Tags : IEquatable<Tags>, IReadOnlyCollection<Tag>
{
private readonly Tag[] _values;
public Tags()
{
_values = [];
}
/// <summary>
/// Will error if there is not tags in contained.
/// </summary>
public readonly Tag First => _values[0];
public Tags(params string[] tags)
{
var arr = new Tag[tags.Length];
for (var i = 0; i < tags.Length; i++)
{
arr[i] = TagRegistry.GetTag(tags[i]);
}
_values = SortAndDeduplicate(arr);
}
public override string ToString() => $"Tags: [{string.Join(", ", _values.Select(t => t.Name))}]";
private static readonly Dictionary<TagArrayKey, Tag[]> _internedTags = [];
public readonly int Count => _values.Length;
private static Tag[] SortAndDeduplicate(Tag[] tags)
{
Array.Sort(tags);
var key = new TagArrayKey(tags);
if (!_internedTags.TryGetValue(key, out var interned))
{
_internedTags[key] = interned = tags;
}
return interned;
}
public Tags(params Tag[] tags)
{
_values = SortAndDeduplicate(tags);
}
public Tags(ISet<Tag> tags) : this(tags.ToArray())
{
}
public Tags With(params Tag[] tags)
{
HashSet<Tag> set = [.. _values, .. tags];
return new Tags(set);
}
public Tags With(params string[] tags)
{
HashSet<Tag> set = [.. _values, .. TagRegistry.GetTags(tags)];
return new Tags(set);
}
public Tags WithOut(params string[] tags)
{
HashSet<Tag> set = [.. _values];
for (int i = 0; i < tags.Length; i++)
{
set.Remove(TagRegistry.GetTag(tags[i]));
}
return new Tags(set);
}
public readonly bool Contains(Tag tag) => Array.BinarySearch(_values, tag) >= 0;
public readonly bool Equals(Tags other) => _values == other._values;
public override readonly bool Equals([NotNullWhen(true)] object? obj) => obj is Tags tags && Equals(tags);
public override readonly int GetHashCode() => _values.GetHashCode();
public readonly IEnumerator<Tag> GetEnumerator()
{
foreach (var item in _values)
{
yield return item;
}
}
readonly IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
public readonly struct TagArrayKey : IEquatable<TagArrayKey>
{
private readonly Tag[] _tags;
public TagArrayKey(Tag[] tags)
{
_tags = tags;
}
public bool Equals(TagArrayKey other)
{
var a = _tags;
var b = other._tags;
if (a == b)
{
return true;
}
if (a == null || b == null)
{
return false;
}
if (a.Length != b.Length)
{
return false;
}
for (var i = 0; i < a.Length; i++)
{
if (a[i] != b[i])
{
return false;
}
}
return true;
}
public override bool Equals(object? obj)
=> obj is TagArrayKey other && Equals(other);
public override int GetHashCode()
{
var hash = new HashCode();
foreach (var tag in _tags)
{
hash.Add(tag._id);
}
return hash.ToHashCode();
}
}
public readonly bool ContainsAll(Tags other)
{
var i = 0;
var j = 0;
while (i < _values.Length && j < other._values.Length)
{
if (_values[i] == other._values[j])
{
i++;
j++;
}
else if (_values[i] < other._values[j])
{
return false;
}
else
{
j++;
}
}
return j == other._values.Length;
}
public readonly bool ContainsAny(Tags other)
{
for (var i = 0; i < _values.Length; i++)
{
if (other.Contains(_values[i]))
{
return true;
}
}
return false;
}
}
public readonly struct Tag : IEquatable<Tag>, IEqualityOperators<Tag, Tag, bool>, IComparisonOperators<Tag, Tag, bool>,
IComparable<Tag>
{
public readonly string Name => TagRegistry.GetName(this);
internal readonly int _id;
internal Tag(int id)
{
Debug.Assert(id >= 0);
_id = id;
}
public override bool Equals([NotNullWhen(true)] object? obj) => obj is Tag tag && Equals(tag);
public bool Equals(Tag other) => _id == other._id;
public override int GetHashCode() => _id;
public override string ToString() => Name;
public int CompareTo(Tag other) => _id.CompareTo(other._id);
public static bool operator >(Tag left, Tag right) => left._id > right._id;
public static bool operator >=(Tag left, Tag right) => left._id >= right._id;
public static bool operator <(Tag left, Tag right) => left._id < right._id;
public static bool operator <=(Tag left, Tag right) => left._id <= right._id;
public static bool operator ==(Tag left, Tag right) => left._id == right._id;
public static bool operator !=(Tag left, Tag right) => left._id != right._id;
}
public static class TagRegistry
{
private static Registry<string> _registry = new();
// private static readonly ConcurrentDictionary<string, Tag> _stringTags = [];
// private static readonly List<string> _tagsStrings = [];
public static Tag GetTag(string name) => new(_registry.GetOrCreate(name));
// {
// name = name.ToLowerInvariant();
// if (_stringTags.TryGetValue(name, out var tag))
// {
// return tag;
// }
// return CreateNewTag(name);
// }
public static string GetName(Tag tag) => _registry.GetEntry(tag._id);//=> tag._id < _tagsStrings.Count ? _tagsStrings[tag._id] : throw new IndexOutOfRangeException($"{nameof(tag)} was somehow created with an value greater then the count of {nameof(_tagsStrings)}. {nameof(tag)}:{tag._id} {nameof(_tagsStrings.Count)}:{_tagsStrings.Count}");
public static Tag[] GetTags(string[] names)
{
var array = new Tag[names.Length];
for (int i = 0; i < names.Length; i++)
{
array[i] = GetTag(names[i]);
}
return array;
}
// private static Tag CreateNewTag(string name)
// {
// Tag tag;
// lock (_tagsStrings)
// {
// tag = new Tag(_tagsStrings.Count);
// _tagsStrings.Add(name);
// }
// _stringTags[name] = tag;
// return tag;
// }
}
public class Registry<TEntry> where TEntry : notnull
{
private readonly Dictionary<TEntry, int> _data = [];
private readonly List<TEntry> _list = [];
private readonly object _lock = new();
public IReadOnlyList<TEntry> Entries => _list;
public void Clear()
{
lock (_lock)
{
_data.Clear();
_list.Clear();
}
}
public int GetOrCreate(TEntry name)
{
lock (_lock)
{
if (_data.TryGetValue(name, out var tag))
{
return tag;
}
return CreateNewKey(name);
}
}
public TEntry GetEntry(int index)
{
lock (_lock)
{
if ((uint)index >= (uint)_list.Count)
{
throw new KeyNotFoundException($"The index {index} you are trying to access does not exist in the registry. The registry only has {_list.Count} of type {typeof(TEntry).Name}s");
}
return _list[index];
}
}
private int CreateNewKey(TEntry name)
{
int index = _list.Count;
_list.Add(name);
_data[name] = index;
return index;
}
}

View File

219
src/Items/ItemECSTest.cs Normal file
View File

@@ -0,0 +1,219 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Arch.Core;
using Arch.Core.Extensions;
using Arch.LowLevel.Jagged;
using FoodFactory;
using FoodFactory.Items;
using FoodFactory.Recipes;
using Godot;
public class Test
{
public void TestECS()
{
using var world = World.Create();
var bucket = new BucketStorage<List<Entity>>();
var pizza = world.Create(new Name("Pizza"), new Temperature(5f), new MarcoNutrients(5, 5, 10), new MicroNutrients([new Vitamin("D", 5)]));
var turkey = world.Create(new Name("Turkey"), new Temperature(5f), new MarcoNutrients(5, 5, 10));
// var componentType = new ComponentType(ComponentRegistry.Size-1, 8);
// ComponentRegistry.Add(typeof(LayeredStack2), componentType);
var query = new QueryDescription().WithAll<Name, Temperature, MarcoNutrients>();
world.Query(in query, (Entity entity, ref Name name, ref Temperature temperature) =>
{
temperature.Kelvin += .1f;
GD.Print(temperature);
});
if (pizza.Has<Temperature>())
{
pizza.Get<Temperature>().Kelvin += .1f;
}
pizza.Add(new IngredientOf(turkey));
var a = new List<Entity>(){pizza};
turkey.Add(new LayeredStack(bucket.Add(a)));
turkey.Add(new LayeredStack2(a));
var list = bucket.GetRef(turkey.Get<LayeredStack>().Handle);
GD.Print(string.Join(',', list));
var list2 = turkey.Get<LayeredStack2>().Children;
turkey.Get<LayeredStack2>().Children.Add(turkey);
GD.Print(string.Join(',', list2));
var r = pizza.Get(typeof(Name));
ComponentRegistry.TryGet<LayeredStack2>(out var type);
GD.Print(type);
var flour = world.Create(new Name("flour"), new Temperature(71, TemperatureUnit.Fahrenheit), new Tags("flour", "wheat"));
var tag2 = new Tags();
world.SubscribeEntityDestroyed((in entity) =>
{
foreach (var item in entity.GetAllComponents())
{
if (item is IEntityContainer container)
{
foreach (var item2 in container.GetEntities())
{
world.Destroy(item2);
}
}
}
}
);
var recipes = new Recipes();
var item = recipes.TESTBlueprint.Factory(new BlueprintContext(){World = world, BluePrintId = new BlueprintId(recipes.TESTBlueprint)});//world.Create(new Name("Potato_Raw"), new Temperature(71f), new Tags("raw", "potato", "vegetable"));
Span<Entity> items = [item];
var context = new RecipeContext(world, items);
var builder = new RecipeResultBuilder(stackalloc bool[10], new ItemBuilder[10]);
// var recipe = new PotatoCookRecipe();
var recipe = recipes.GetRecipes(new RecipeAction("cook"), 1, [item.Get<Tags>()], [])[0].Recipe;
if (recipe.CanProcess(context))
{
var result = recipe.Process(context, ref builder);
var list3 = new List<Entity>();
for (int i = 0; i < result.CreateLength; i++)
{
list3.Add(result.Create[i](in context));
}
if (result.RemoveLength > 0)
{
for (int i = 0; i < result.RemoveLength; i++)
{
GD.Print(result.Remove[i]);
if (result.Remove[i])
{
world.Destroy(items[i]);
}
}
}
GD.Print(string.Join(',',list3[0].GetAllComponents()));
GD.Print(items[0].IsAlive());
}
}
}
internal interface IEntityContainer
{
IEnumerable<Entity> GetEntities();
}
public record struct Name(string Value);
public record struct MarcoNutrients(float Fat, float Protein, float Carbohydrate);
public record struct MicroNutrients(List<Vitamin> Vitamins);
public record struct Vitamin(string Id, float Amount);
public record struct IngredientOf(Entity Source);
public record struct LayeredStack(Handle Handle);
public record struct LayeredStack2(List<Entity> Children);
public record struct Handle(int Index, int Version);
public class BucketStorage<T>
{
private struct Slot
{
public T Value;
public int Version;
public bool Occupied;
}
private Slot[] _slots = new Slot[1];
private Stack<int> _freeIndices = new();
// Allocate a new slot
public Handle Add(T value)
{
if (_freeIndices.Count > 0)
{
int index = _freeIndices.Pop();
var slot = _slots[index];
slot.Value = value;
slot.Occupied = true;
slot.Version++; // bump version on reuse
_slots[index] = slot;
return new Handle { Index = index, Version = slot.Version };
}
else
{
var slot = new Slot
{
Value = value,
Version = 1,
Occupied = true
};
Array.Resize(ref _slots, _slots.Length + 1);
_slots[^1] = slot;
return new Handle
{
Index = _slots.Length - 1,
Version = slot.Version
};
}
}
// Safe access
public bool TryGet(Handle handle, out T value)
{
if (handle.Index < 0 || handle.Index >= _slots.Length)
{
value = default;
return false;
}
var slot = _slots[handle.Index];
if (!slot.Occupied || slot.Version != handle.Version)
{
value = default;
return false;
}
value = slot.Value;
return true;
}
// Direct (unsafe-ish) access if you trust the handle
public ref T GetRef(Handle handle)
{
return ref _slots[handle.Index].Value;
}
// Remove and recycle index
public bool Remove(Handle handle)
{
if (handle.Index < 0 || handle.Index >= _slots.Length)
return false;
var slot = _slots[handle.Index];
if (!slot.Occupied || slot.Version != handle.Version)
return false;
slot.Occupied = false;
slot.Version++; // invalidate old handles
_slots[handle.Index] = slot;
_freeIndices.Push(handle.Index);
return true;
}
}

49
src/Items/itemtest.jsonc Normal file
View File

@@ -0,0 +1,49 @@
[
{
"name" : "wheat_to_flour",
"description" : "Wheat to Flour",
"inputs" : [{"tag" : "wheat"}],
"actions" : ["mill"],//should be a list of actions, bacily addioanl tags, used for reduceing howm may recipes to look for
"effects" : [
{
"effect_type" : " replace_tag",
"target" : "wheat",
"result" : "flour"
}
]
},
{
"name" : "carmlize",
"description" : "Camalize sugars",
"inputs" : [{"tag" : "meat"},{"tag" : "sugar"}, {"tag": "bread"}],
"actions" : ["heat"],// maybe incude addioanl aip contox for like heat applied, all need to be preset to be valid recipe
"expr" : [
{
"target": "Nutration.Flavors.Camalize",
"condition" : "Temperature.F > 350",// i want to be able to have nested expressions that would branch
"op" :"add",
"expr" : [
"MarcoNutrients.Sugar * delta * someMathForRateOfCamrlization",//exprestion could branch and allow for more complex recipes, or be step and maybe variables
]
},
]
},
{
"name" : "stack",
"description" : "Burned the meat",
"inputs" : [{"tag" : "stackable"}],//realticly could be any item, but allow cetin thigns to stack makes senes
//Also need an way to allow mutple item inputs, like stacking a meat slce on slice of bread
"actions" : ["stack"],// maybe incude addioanl aip contox for like heat applied, all need to be preset to be valid recipe
"expr" : [
{
"target": "Nutration.Flavors.Camalize",
"condition" : "Temperature.F > 350",// i want to be able to have nested expressions that would branch
"op" :"add",
"expr" : [
"MarcoNutrients.Sugar * delta * someMathForRateOfCamrlization",//exprestion could branch and allow for more complex recipes, or be step and maybe variables
]
},
]
},
]

494
src/Math/Temperature.cs Normal file
View File

@@ -0,0 +1,494 @@
//https://www.codeproject.com/Articles/311333/A-Csharp-Temperature-Struct
namespace FoodFactory;
using System;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Numerics;
/// <summary>
/// Options for temperature measurement units.
/// </summary>
public enum TemperatureUnit
{
/// <summary>
/// The SI base unit of thermodynamic temperature,
/// equal in magnitude to the degree Celsius.
/// </summary>
Kelvin,
/// <summary>
/// A scale of temperature on which water freezes
/// at 0° and boils at 100° under standard conditions.
/// </summary>
Celsius,
/// <summary>
/// A scale of temperature on which water freezes at 32°
/// and boils at 212° under standard conditions.
/// </summary>
Fahrenheit
}
/// <summary>
/// A temperature value.
/// </summary>
public struct Temperature : IFormattable, IComparable,
IComparable<Temperature>, IEquatable<Temperature>,
IAdditionOperators<Temperature, TemperatureDelta, Temperature>,
ISubtractionOperators<Temperature, Temperature, TemperatureDelta>,
ISubtractionOperators<Temperature, TemperatureDelta, TemperatureDelta>,
IEqualityOperators<Temperature, Temperature, bool>,
IComparisonOperators<Temperature, Temperature, bool>
{
private double _kelvin;
/// <summary>
/// Creates a new temperature with the specified value in Kelvin.
/// </summary>
/// <param name="kelvin">The value of the temperature.</param>
public Temperature(double kelvin) : this() { _kelvin = kelvin; }
/// <summary>
/// Creates a new temperature with the specified value in the
/// specified unit of measurement.
/// </summary>
/// <param name="temperature">The value of the temperature.</param>
/// <param name="unit">The unit of measurement that defines how
/// the <paramref name="temperature"/> value is used.</param>
public Temperature(double temperature, TemperatureUnit unit)
: this()
{
switch (unit)
{
case TemperatureUnit.Kelvin:
_kelvin = temperature;
break;
case TemperatureUnit.Celsius:
Celsius = temperature;
break;
case TemperatureUnit.Fahrenheit:
Fahrenheit = temperature;
break;
default:
throw new ArgumentException(
"The temperature unit '" + unit.ToString() + "' is unknown.");
}
}
/// <summary>
/// Gets or sets the temperature value in Kelvin.
/// </summary>
public double Kelvin
{
readonly get => _kelvin;
set => _kelvin = value;
}
/// <summary>
/// Gets or sets the temperature value in Celsius.
/// </summary>
public double Celsius
{
readonly get => KelvinToCelsius(_kelvin); set => _kelvin = CelsiusToKelvin(value);
}
/// <summary>
/// Gets or sets the temperature value in Fahrenheit.
/// </summary>
public double Fahrenheit
{
readonly get => KelvinToFahrenheit(_kelvin); set => _kelvin = FahrenheitToKelvin(value);
}
/// <summary>
/// Gets the temperature value in the specified unit of measurement.
/// </summary>
/// <param name="unit">The unit of measurement
/// in which the temperature should be retrieved.</param>
/// <returns>The temperature value in the specified
/// <paramref name="unit"/>.</returns>
public readonly double ValueIn(TemperatureUnit unit) => unit switch
{
TemperatureUnit.Kelvin => _kelvin,
TemperatureUnit.Celsius => Celsius,
TemperatureUnit.Fahrenheit => Fahrenheit,
_ => throw new ArgumentException(
"Unknown temperature unit '" + unit.ToString() + "'."),
};
/// <summary>
/// Returns a string representation of the temperature value.
/// </summary>
/// <param name="format">
/// A single format specifier that indicates how to format the value of this
/// temperature. The format parameter can be "G",
/// "C", "F", or "K". If format
/// is null or the empty string (""), "G" is used.
/// </param>
/// <param name="provider">
/// An IFormatProvider reference that supplies culture-specific formatting
/// services.
/// </param>
/// <returns>A string representation of the temperature.</returns>
/// <exception cref="FormatException">
/// The value of format is not null, the empty string (""), "G", "C", "F", or
/// "K".
/// </exception>
public readonly string ToString(string? format, IFormatProvider? provider)
{
if (string.IsNullOrEmpty(format))
{
format = "G";
}
provider ??= CultureInfo.CurrentCulture;
return format switch
{
"G" or "C" or "g" or "c" => Celsius.ToString("F2", provider) + " °C",
"F" or "f" => Fahrenheit.ToString("F2", provider) + " °F",
"K" or "k" => _kelvin.ToString("F2", provider) + " K",
_ => throw new FormatException(
string.Format("The {0} format string is not supported.", format)),
};
}
/// <summary>
/// Returns a string representation of the temperature value.
/// </summary>
/// <param name="format">
/// A single format specifier that indicates how to format the value of this
/// temperature. The format parameter can be "G",
/// "C", "F", or "K". If format
/// is null or the empty string (""), "G" is used.
/// </param>
/// <returns>A string representation of the temperature.</returns>
/// <exception cref="FormatException">
/// The value of format is not null,
/// the empty string (""), "G", "C", "F", or
/// "K".
/// </exception>
public readonly string ToString(string format) => ToString(format, null);
/// <summary>
/// Returns a string representation of the temperature value.
/// </summary>
/// <returns>A string representation of the temperature.</returns>
public override readonly string ToString() => ToString(null, null);
/// <summary>
/// Returns a string representation of the temperature value.
/// </summary>
/// <param name="unit">
/// The temperature unit as which the temperature value should be displayed.
/// </param>
/// <param name="provider">
/// An IFormatProvider reference that supplies culture-specific formatting
/// services.
/// </param>
/// <returns>A string representation of the temperature.</returns>
public readonly string ToString(TemperatureUnit unit, IFormatProvider? provider)
{
return unit switch
{
TemperatureUnit.Celsius => ToString("C", provider),
TemperatureUnit.Fahrenheit => ToString("F", provider),
TemperatureUnit.Kelvin => ToString("K", provider),
_ => throw new FormatException("The temperature unit '" +
unit.ToString() + "' is unknown."),
};
}
/// <summary>
/// Returns a string representation of the temperature value.
/// </summary>
/// <param name="unit">
/// The temperature unit as which the temperature value should be displayed.
/// </param>
/// <returns>A string representation of the temperature.</returns>
public readonly string ToString(TemperatureUnit unit) => ToString(unit, null);
/// <summary>
/// Compares this instance to a specified Temperature object and returns an indication
/// of their relative values.
/// </summary>
/// <param name="value">A Temperature object
/// to compare to this instance.</param>
/// <returns>
/// A signed number indicating the relative values of this instance and value.
/// Value Description A negative integer This instance is less than value. Zero
/// This instance is equal to value. A positive integer This instance is greater
/// than value.
/// </returns>
public readonly int CompareTo(Temperature value) => _kelvin.CompareTo(value._kelvin);
/// <summary>
/// Compares this instance to a specified object and returns an indication of
/// their relative values.
/// </summary>
/// <param name="value">An object to compare, or null.</param>
/// <returns>
/// A signed number indicating the relative values of this instance and value.
/// Value Description A negative integer This instance is less than value. Zero
/// This instance is equal to value. A positive integer This instance is greater
/// than value, or value is null.
/// </returns>
/// <exception cref="ArgumentException">
/// The value is not a Temperature.
/// </exception>
public readonly int CompareTo(object? value)
{
if (value == null)
{
return 1;
}
if (value is not Temperature)
{
throw new ArgumentException($"Can not compare {value.GetType()} and {GetType()}");
}
return CompareTo((Temperature)value);
}
/// <summary>
/// Determines whether or not the given temperature is considered equal to this instance.
/// </summary>
/// <param name="value">The temperature to compare to this instance.</param>
/// <returns>True if the temperature is considered equal
/// to this instance. Otherwise, false.</returns>
public readonly bool Equals(Temperature value) => _kelvin == value._kelvin;
/// <summary>
/// Determines whether or not the given object is considered equal to the temperature.
/// </summary>
/// <param name="value">The object to compare to the temperature.</param>
/// <returns>True if the object is considered equal
/// to the temperature. Otherwise, false.</returns>
public override readonly bool Equals([NotNullWhen(true)] object? value) => value is Temperature temp && Equals(temp);
/// <summary>
/// Returns the hash code for this instance.
/// </summary>
/// <returns>A 32-bit signed integer hash code.</returns>
public override readonly int GetHashCode() => _kelvin.GetHashCode();
/// <summary>
/// Determines the equality of two temperatures.
/// </summary>
/// <param name="t1">The first temperature to be compared.</param>
/// <param name="t2">The second temperature to be compared.</param>
/// <returns>True if the temperatures are equal. Otherwise, false.</returns>
public static bool operator ==(Temperature t1, Temperature t2) => t1.Equals(t2);
/// <summary>
/// Determines the inequality of two temperatures.
/// </summary>
/// <param name="t1">The first temperature to be compared.</param>
/// <param name="t2">The second temperature to be compared.</param>
/// <returns>True if the temperatures are NOT equal. Otherwise, false.</returns>
public static bool operator !=(Temperature t1, Temperature t2) => !t1.Equals(t2);
/// <summary>
/// Determines whether one temperature is considered greater than another.
/// </summary>
/// <param name="t1">The first temperature to be compared.</param>
/// <param name="t2">The second temperature to be compared.</param>
/// <returns>True if the first temperature is greater than the second.
/// Otherwise, false.</returns>
public static bool operator >(Temperature t1, Temperature t2) => t1._kelvin > t2._kelvin;
/// <summary>
/// Determines whether one temperature is considered less than another.
/// </summary>
/// <param name="t1">The first temperature to be compared.</param>
/// <param name="t2">The second temperature to be compared.</param>
/// <returns>True if the first temperature is less than the second.
/// Otherwise, false.</returns>
public static bool operator <(Temperature t1, Temperature t2) => t1._kelvin < t2._kelvin;
/// <summary>
/// Determines whether one temperature is considered greater to or equal to another.
/// </summary>
/// <param name="t1">The first temperature to be compared.</param>
/// <param name="t2">The second temperature to be compared.</param>
/// <returns>
/// True if the first temperature is greater to or equal to the second. Otherwise, false.
/// </returns>
public static bool operator >=(Temperature t1, Temperature t2) => t1._kelvin >= t2._kelvin;
/// <summary>
/// Determines whether one temperature is considered less than or equal to another.
/// </summary>
/// <param name="t1">The first temperature to be compared.</param>
/// <param name="t2">The second temperature to be compared.</param>
/// <returns>
/// True if the first temperature is less than or equal to the second. Otherwise, false.
/// </returns>
public static bool operator <=(Temperature t1, Temperature t2) => t1._kelvin <= t2._kelvin;
/// <summary>
/// Adds two instances of the temperature object.
/// </summary>
/// <param name="t1">The temperature on the left-hand side of the operator.
/// </param>
/// <param name="t2">The temperature on the right-hand side of the operator.
/// </param>
/// <returns>The sum of the two temperatures.</returns>
public static Temperature operator +(Temperature t1, TemperatureDelta t2) => new(t1._kelvin + t2.KelvinDelta);
/// <summary>
/// Subtracts one instance from another.
/// </summary>
/// <param name="t1">The temperature on the left-hand side of the operator.
/// </param>
/// <param name="t2">The temperature on the right-hand side of the operator.
/// </param>
/// <returns>The difference of the two temperatures.</returns>
public static TemperatureDelta operator -(Temperature t1, Temperature t2) => new(t1._kelvin - t2._kelvin);
public static TemperatureDelta operator -(Temperature left, TemperatureDelta right) => new(left._kelvin - right.KelvinDelta);
/// <summary>
/// Converts a Kelvin temperature value to Celsius.
/// </summary>
/// <param name="kelvin">The Kelvin value to convert to Celsius.
/// </param>
/// <returns>The Kelvin value in Celsius.</returns>
public static double KelvinToCelsius(double kelvin) => kelvin - 273.15;
/// <summary>
/// Converts a Celsius value to Kelvin.
/// </summary>
/// <param name="celsius">The Celsius value to convert to Kelvin.
/// </param>
/// <returns>The Celsius value in Kelvin.</returns>
public static double CelsiusToKelvin(double celsius) => celsius + 273.15;
/// <summary>
/// Converts a Kelvin value to Fahrenheit.
/// </summary>
/// <param name="kelvin">The Kelvin value to convert to Fahrenheit.
/// </param>
/// <returns>The Kelvin value in Fahrenheit.</returns>
public static double KelvinToFahrenheit(double kelvin) => (kelvin * 9 / 5) - 459.67;
/// <summary>
/// Converts a Fahrenheit value to Kelvin.
/// </summary>
/// <param name="fahrenheit">The Fahrenheit value to convert to Kelvin.
/// </param>
/// <returns>The Fahrenheit value in Kelvin.</returns>
public static double FahrenheitToKelvin(double fahrenheit) => (fahrenheit + 459.67) * 5 / 9;
/// <summary>
/// Converts a Fahrenheit value to Celsius.
/// </summary>
/// <param name="fahrenheit">The Fahrenheit value to convert to Celsius.
/// </param>
/// <returns>The Fahrenheit value in Celsius.</returns>
public static double FahrenheitToCelsius(double fahrenheit) => (fahrenheit - 32) * 5 / 9;
/// <summary>
/// Converts a Celsius value to Fahrenheit.
/// </summary>
/// <param name="celsius">The Celsius value to convert to Fahrenheit.
/// </param>
/// <returns>The Celsius value in Fahrenheit.</returns>
public static double CelsiusToFahrenheit(double celsius) => (celsius * 9 / 5) + 32;
}
public readonly struct TemperatureDelta : IFormattable, IComparable,
IComparable<TemperatureDelta>, IEquatable<TemperatureDelta>,
IComparisonOperators<TemperatureDelta, TemperatureDelta, bool>,
IEqualityOperators<TemperatureDelta, TemperatureDelta, bool>,
IAdditionOperators<TemperatureDelta, TemperatureDelta, TemperatureDelta>,
ISubtractionOperators<TemperatureDelta, TemperatureDelta, TemperatureDelta>,
IMultiplyOperators<TemperatureDelta, double, TemperatureDelta>,
IDivisionOperators<TemperatureDelta, double, TemperatureDelta>
{
private readonly double _kelvinDelta;
public TemperatureDelta(double kelvin)
{
_kelvinDelta = kelvin;
}
public readonly double KelvinDelta => _kelvinDelta;
public readonly double FahrenheitDelta => CelsiusToFahrenheitDelta(_kelvinDelta);
public readonly double CelsiusDelta => _kelvinDelta;
public TemperatureDelta(double temperature, TemperatureUnit unit)
{
_kelvinDelta = unit switch
{
TemperatureUnit.Celsius => temperature,
TemperatureUnit.Fahrenheit => FahrenheitToKelvinDelta(temperature),
TemperatureUnit.Kelvin => temperature,
_ => throw new ArgumentException($"Unit {unit} is not a valid {nameof(TemperatureUnit)}"),
};
}
public static double FahrenheitToKelvinDelta(double fahrenheit) => fahrenheit * 5 / 9;
public static double KelvinToFahrenheitDelta(double kelvin) => kelvin * 9 / 5;
public static double FahrenheitToCelsiusDelta(double fahrenheit) => FahrenheitToKelvinDelta(fahrenheit);
public static double CelsiusToFahrenheitDelta(double celsius) => KelvinToFahrenheitDelta(celsius);
public bool Equals(TemperatureDelta other) => _kelvinDelta == other._kelvinDelta;
public override bool Equals([NotNullWhen(true)] object? obj) => obj is TemperatureDelta delta && Equals(delta);
public override int GetHashCode() => _kelvinDelta.GetHashCode();
public readonly string ToString(string? format, IFormatProvider? provider)
{
if (string.IsNullOrEmpty(format))
{
format = "G";
}
provider ??= CultureInfo.CurrentCulture;
return format switch
{
"G" or "C" or "g" or "c" => CelsiusDelta.ToString("F2", provider) + " °C",
"F" or "f" => FahrenheitDelta.ToString("F2", provider) + " °F",
"K" or "k" => _kelvinDelta.ToString("F2", provider) + " K",
_ => throw new FormatException(
string.Format("The {0} format string is not supported.", format)),
};
}
public readonly string ToString(string format) => ToString(format, null);
public override string ToString() => ToString(null, null);
public readonly int CompareTo(TemperatureDelta value) => _kelvinDelta.CompareTo(value._kelvinDelta);
public readonly int CompareTo(object? value)
{
if (value == null)
{
return 1;
}
if (value is not TemperatureDelta)
{
throw new ArgumentException(
$"Object must be of type {nameof(TemperatureDelta)}",
nameof(value));
}
return CompareTo((TemperatureDelta)value);
}
public static TemperatureDelta operator +(TemperatureDelta left, TemperatureDelta right) => new(left._kelvinDelta + right._kelvinDelta);
public static TemperatureDelta operator -(TemperatureDelta left, TemperatureDelta right) => new(left._kelvinDelta - right._kelvinDelta);
public static TemperatureDelta operator *(TemperatureDelta left, double right) => new(left._kelvinDelta * right);
public static TemperatureDelta operator /(TemperatureDelta left, double right) => new(left._kelvinDelta / right);
public static bool operator >(TemperatureDelta left, TemperatureDelta right) => left._kelvinDelta > right._kelvinDelta;
public static bool operator >=(TemperatureDelta left, TemperatureDelta right) => left._kelvinDelta >= right._kelvinDelta;
public static bool operator <(TemperatureDelta left, TemperatureDelta right) => left._kelvinDelta < right._kelvinDelta;
public static bool operator <=(TemperatureDelta left, TemperatureDelta right) => left._kelvinDelta <= right._kelvinDelta;
public static bool operator ==(TemperatureDelta left, TemperatureDelta right) => left._kelvinDelta == right._kelvinDelta;
public static bool operator !=(TemperatureDelta left, TemperatureDelta right) => left._kelvinDelta != right._kelvinDelta;
}

413
src/Recipes/Recipes.cs Normal file
View File

@@ -0,0 +1,413 @@
namespace FoodFactory.Recipes;
using System;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Arch.Core;
using Arch.Core.Extensions;
using FastExpressionCompiler.ImTools;
using FoodFactory.Items;
using Godot;
using Parlot.Fluent;
using SJK.Functional;
public class Recipes
{
private readonly Dictionary<RecipeAction, Dictionary<int, RecipeBucket>> _actions = [];
private readonly Dictionary<RecipeName, Recipe> _recipes = [];
public Option<Recipe> GetRecipeByName(RecipeName name) => _recipes.GetValue(name).ToStructOption();
public Blueprint TESTBlueprint;
public Recipes()
{
//TODO Utilize the singtaure ssytem to create the item, the have a post proccess step that would be for setting propertied
var rawPotatoBlueprint = TESTBlueprint = new Blueprint("Potato", static ctx => ctx.World.Create(new Name("Potato_raw"), new Tags("potato", "raw", "vegetable"), new Temperature(71, TemperatureUnit.Fahrenheit), ctx.BluePrintId));
var recipe = new PotatoCookRecipe();
_recipes.Add(new RecipeName(recipe.Name), recipe);
var compiledRecipe = new CompiledRecipe() { IsOrdered = true, RecipeRef = recipe, InputBlueprints = [new BlueprintId(rawPotatoBlueprint)], ItemEntries = [new CompiledRecipe.ItemEntry() { InputTags = recipe.Inputs[0], ExcludeTags = recipe.Exclude[0], RequireAll = recipe.RequireAll[0] }] };
_actions.Add(new("cook"), new Dictionary<int, RecipeBucket>() { [1] = new RecipeBucket() {Single = new Dictionary<BlueprintId, CompiledRecipe[]>(){[new BlueprintId(rawPotatoBlueprint)] = [compiledRecipe] }, Unordered = [compiledRecipe] } });
}
public List<(Recipe Recipe, int[]? Mapping)> GetRecipes(RecipeAction action, int inputs, Tags[] itemTags, BlueprintId[] blueprintIds)
{
var list = new List<(Recipe, int[])>();
Debug.Assert(inputs > 0);
Debug.Assert(inputs == itemTags.Length);
if (!_actions.TryGetValue(action, out var inputAmount))
{
return null;
}
if (!inputAmount.TryGetValue(inputs, out var blueprints))
{
return null;
}
var recipes = blueprints.Resolve(blueprintIds);
var mapping = new int[itemTags.Length];
var used = new bool[itemTags.Length];
for (int i = 0; i < recipes.Length; i++)
{
var item = recipes[i];
if (inputs == 1 && IsValid(itemTags[0], item.ItemEntries[0]))
{
list.Add((item.RecipeRef, null));
}
else if (item.IsOrdered)
{
for (int ii = 0; ii < itemTags.Length; ii++)
{
if (!IsValid(itemTags[ii], item.ItemEntries[ii]))
{
continue;
}
}
list.Add((item.RecipeRef, null));
}
else if (CanPossiblyMatch(itemTags, item))
{
if (Match(itemTags, item.ItemEntries, 0, used, mapping))
{
list.Add((item.RecipeRef, mapping));
}
}
}
return list;
}
private static bool IsValid(Tags tags, CompiledRecipe.ItemEntry compiled)
{
if (!(compiled.RequireAll ? tags.ContainsAll(compiled.InputTags) : tags.ContainsAny(compiled.InputTags)))
{
return false;
}
if (tags.ContainsAny(compiled.ExcludeTags))
{
return false;
}
return true;
}
private static bool CanPossiblyMatch(Tags[] items, CompiledRecipe recipe)
{
foreach (var entry in recipe.ItemEntries)
{
bool found = false;
foreach (var item in items)
{
if (IsValid(item, entry))
{
found = true;
break;
}
}
if (!found)
return false;
}
return true;
}
private static bool Match(
Tags[] items,
CompiledRecipe.ItemEntry[] entries,
int depth,
Span<bool> used,
Span<int> mapping)
{
if (depth == entries.Length)
{
return true;
}
for (int i = 0; i < items.Length; i++)
{
if (used[i])
{
continue;
}
if (!IsValid(items[i], entries[depth]))
{
continue;
}
used[i] = true;
mapping[depth] = i;
if (Match(items, entries, depth + 1, used, mapping))
{
return true;
}
used[i] = false;
}
return false;
}
private bool MatchArrays<T, T2>(T[] inputs, T2[] test, Func<T, T2, bool> isValid)
{
Debug.Assert(inputs.Length == test.Length);
var used = new bool[inputs.Length];
bool backTrace(int index)
{
if (index == inputs.Length)
{
return true;
}
for (int i = 0; i < test.Length; i++)
{
if (used[i])
{
continue;
}
if (!isValid(inputs[index], test[i]))
{
continue;
}
used[i] = true;
if (backTrace(index + 1))
{
return true;
}
used[i] = false;
}
return false;
}
return backTrace(0);
}
private int[]? MatchArraysAndMap<T, T2>(T[] inputs, T2[] test, Func<T, T2, bool> isValid)
{
Debug.Assert(inputs.Length == test.Length);
var used = new bool[inputs.Length];
var mapping = new int[inputs.Length];
Array.Fill(mapping, -1);
int[]? backTrace(int index)
{
if (index == inputs.Length)
{
return [.. mapping];
}
for (int i = 0; i < test.Length; i++)
{
if (used[i])
{
continue;
}
if (!isValid(inputs[index], test[i]))
{
continue;
}
used[i] = true;
mapping[index] = i;
var result = backTrace(index + 1);
if (result is not null)
{
return result;
}
used[i] = false;
mapping[index] = -1;
}
return null;
}
return backTrace(0);
}
}
public abstract record Recipe(string Name, BlueprintId[]? BlueprintIds, bool[] RequireAll, Tags[] Inputs, Tags[] Exclude)
{
public abstract bool CanProcess(in RecipeContext context);
public abstract RecipeResult Process(in RecipeContext context, ref RecipeResultBuilder builder);
}
public record PotatoCookRecipe() : Recipe(Name: "Potato_Cook", BlueprintIds: null, RequireAll: [false], Inputs: [new Tags("potato")], Exclude: [new Tags()])
{
public override bool CanProcess(in RecipeContext context)
{
if (!context.Entity[0].Has<Temperature>())
{
return false;
}
return true;
}
public override RecipeResult Process(in RecipeContext context, ref RecipeResultBuilder builder)
{
builder.AddRemove(true);
builder.AddCreate(NewPotato);
return builder.Build();
}
private Entity NewPotato(in RecipeContext context)
{
Span<Entity> span = stackalloc Entity[1];
var signature = context.World.GetSignature(context.Entity[0]);
context.World.Create(span, signature, 1);
span[0].Set(new Name("Cooked_Potato"));
span[0].Set(context.Entity[0].Get<Tags>().With("cooked").WithOut("raw"));
span[0].Set(context.Entity[0].Get<Temperature>());
return span[0];
}
}
public ref struct RecipeResultBuilder
{
private Span<bool> _remove;
private Span<ItemBuilder> _create;
private int _removeCount;
private int _createCount;
public RecipeResultBuilder(Span<bool> remove, Span<ItemBuilder> create)
{
_remove = remove;
_create = create;
_removeCount = 0;
_createCount = 0;
}
public void AddRemove(bool value) => _remove[_removeCount++] = value;
public void AddCreate(ItemBuilder item) => _create[_createCount++] = item;
public RecipeResult Build()
=> new RecipeResult(
_remove.Slice(0, _removeCount),
_create.Slice(0, _createCount));
}
public delegate Entity ItemBuilder(in RecipeContext ctx);
public readonly ref struct RecipeContext
{
public readonly World World;
public RecipeContext(World world, Span<Entity> entity) : this()
{
World = world;
Entity = entity;
}
public readonly Span<Entity> Entity;
// optional later:
// public readonly Machine Machine;
// public readonly float Delta;
}
public readonly ref struct RecipeResult
{
public readonly int RemoveLength => Remove.Length;
public readonly int CreateLength => Create.Length;
public readonly ReadOnlySpan<bool> Remove; // destroy original
public RecipeResult(ReadOnlySpan<bool> remove, ReadOnlySpan<ItemBuilder> create) : this()
{
Remove = remove;
Create = create;
Mutate = null;
}
public readonly ReadOnlySpan<ItemBuilder> Create; // multi-output
public readonly Action<RecipeContext>? Mutate; // component changes
public RecipeResult(Action<RecipeContext> mutate) : this()
{
Mutate = mutate;
}
}
public struct CompiledRecipe
{
public Recipe RecipeRef;
public BlueprintId[] InputBlueprints;
public ItemEntry[] ItemEntries;
public bool IsOrdered;
public struct ItemEntry
{
public bool RequireAll;
public Tags InputTags;
public Tags ExcludeTags;
}
}
// public record SingleItemRecipe(string Name, Tags Required, Tags Exclude) : Recipe(Name)
// {
// }
// public record MultipleItemRecipe(string Name, params ItemInput[] Inputs) : Recipe(Name)
// {
// }
public readonly record struct RecipeName
{
public readonly string Name;
public RecipeName(string name)
{
Name = name.ToLowerInvariant();
}
public static implicit operator string(RecipeName name) => name.Name;
}
public readonly record struct RecipeAction
{
public readonly string Value;
public RecipeAction(string value)
{
Value = value.ToLowerInvariant();
}
public static implicit operator string(RecipeAction name) => name.Value;
}
public readonly record struct BlueprintId
{
public readonly int Id;
public Blueprint Blueprint => _registry.GetEntry(Id);
private static Registry<Blueprint> _registry = new();
public BlueprintId(Blueprint blueprint)
{
Id = _registry.GetOrCreate(blueprint);
}
}
public record Blueprint(string Name, Func<BlueprintContext, Entity> Factory)
{
}
public struct BlueprintContext
{
public World World;
public BlueprintId BluePrintId;
}
public sealed class RecipeBucket
{
// 1 input → direct blueprint lookup
public Dictionary<BlueprintId, CompiledRecipe[]> Single = [];
// ordered multi-input → first item is anchor
public Dictionary<BlueprintId, CompiledRecipe[]> Ordered = [];
// fallback (unordered / no blueprint)
public CompiledRecipe[] Unordered = [];
/// <summary>
/// Resloves Blueprints, if span is length 0, tehn Unordred is returned imdetly
/// </summary>
/// <param name="items"></param>
/// <returns></returns>
public ReadOnlySpan<CompiledRecipe> Resolve(Span<BlueprintId> items)
{
if (items.Length == 0)
{
return Unordered;
}
if (items.Length == 1)
{
if (Single.TryGetValue(items[0], out var r))
return r;
return Unordered;
}
var main = items[0];
if (Ordered.TryGetValue(main, out var ordered))
{
return ordered;
}
return Unordered;
}
}