Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions FactorioCalc.sln
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Yafc.I18n", "Yafc.I18n\Yafc
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Yafc.I18n.Generator", "Yafc.I18n.Generator\Yafc.I18n.Generator.csproj", "{E8A28A02-99C4-41D3-99E3-E6252BD116B7}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Yafc.Parser.Tests", "Yafc.Parser.Tests\Yafc.Parser.Tests.csproj", "{E7D5C910-550A-4028-A866-C536F1FAD645}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -68,6 +70,10 @@ Global
{E8A28A02-99C4-41D3-99E3-E6252BD116B7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E8A28A02-99C4-41D3-99E3-E6252BD116B7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E8A28A02-99C4-41D3-99E3-E6252BD116B7}.Release|Any CPU.Build.0 = Release|Any CPU
{E7D5C910-550A-4028-A866-C536F1FAD645}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E7D5C910-550A-4028-A866-C536F1FAD645}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E7D5C910-550A-4028-A866-C536F1FAD645}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E7D5C910-550A-4028-A866-C536F1FAD645}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down
2 changes: 1 addition & 1 deletion Yafc.Model.Tests/LuaDependentTestHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ internal static Project GetProjectForLua(string targetStreamName = null) {
// Read the four lua files and generate an empty project.
Project project;
Helper helper = new();
using (LuaContext context = new LuaContext()) {
using (LuaContext context = new LuaContext(new(2, 0, 7))) {
byte[] bytes = File.ReadAllBytes("Data/Sandbox.lua");
context.Exec(bytes, "*", "");

Expand Down
10 changes: 0 additions & 10 deletions Yafc.Model/Data/DataClasses.cs
Original file line number Diff line number Diff line change
Expand Up @@ -383,16 +383,6 @@ public Item() {
Recipe? getSpoilRecipe() => Database.recipes.all.OfType<Mechanics>().SingleOrDefault(r => r.name == "spoil." + name);
}

/// <summary>
/// The prototypes in this array will be loaded in order, before any other prototypes.
/// This should correspond to the prototypes for subclasses of item, with more derived classes listed before their base classes.
/// e.g. If ItemWithHealth and ModuleWithHealth C# classes were added, all prototypes for both classes must be listed here.
/// Ignoring style restrictions, the prototypes could be listed in any order, provided all ModuleWithHealth prototype(s) are listed before "module".
/// </summary>
/// <remarks>This forces modules to be loaded before other items, since deserialization otherwise creates Item objects for all spoil results.
/// It does not protect against modules that spoil into other modules, but one hopes people won't do that.</remarks>
internal static string[] ExplicitPrototypeLoadOrder { get; } = ["ammo", "module"];

public Item? fuelResult { get; internal set; }
public int stackSize { get; internal set; }
public Entity? placeResult { get; internal set; }
Expand Down
48 changes: 48 additions & 0 deletions Yafc.Parser.Tests/MathExpressionTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
using static Yafc.Parser.MathExpression;

namespace Yafc.Parser.Tests;

public class MathExpressionTests {
// Note that these tests are cumulative. If there are test failures, fix tokenization first, transpilation second, and evaluation last.
[Theory]
[MemberData(nameof(TokenizationData))]
public void TestTokenization(string input, List<object?> output)
=> Assert.Equivalent(output, Tokenize(input));

[Theory]
[MemberData(nameof(TranspilationData))]
public void TestTranspilation(string input, string? output)
=> Assert.Equal(output, Transpile(Tokenize(input)));

[Theory]
[MemberData(nameof(EvaluationData))]
public void TestEvaluation(string input, float output)
=> Assert.Equal(output, Evaluate(input, null));

public static TheoryData<string, List<object?>> TokenizationData => new() {
{ "a * b", ["a", Token.Asterisk, "b"] },
{ "call(x, y)", ["call", Token.OpenParen, "x", Token.Comma, "y", Token.CloseParen] },
{ "x^y^z", ["x", Token.Caret, "y", Token.Caret, "z"] },
{ "0x1234 + 123.456", [4660f, Token.Plus, 123.456f] },
{ "log2(5)", ["log2", Token.OpenParen, 5f, Token.CloseParen] },
{ "$", [null] },
};

public static TheoryData<string, string?> TranspilationData => new() {
{ "a * b", "@a*@b;" },
{ "call(x, y)", "@call(@x,@y);" },
{ "x^y^z", "@x..@y..@z;" },
{ "0x1234 + 123.456", "4660+123.456;" },
{ "log2(5)", "@log2(5);" },
{ "$", null },
};

public static TheoryData<string, float> EvaluationData => new() {
{ "1+2*3", 7 },
{ "2^3^2", 512 },
{ "2^(2^3)", 256 },
{ "(2^3)^2", 64 },
{ "0x1234 * 123.456", 0x1234 * 123.456f },
{ "log2(5)", MathF.Log2(5) },
};
}
25 changes: 25 additions & 0 deletions Yafc.Parser.Tests/Yafc.Parser.Tests.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageReference Include="xunit" Version="2.5.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.3" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Yafc.Parser\Yafc.Parser.csproj" />
</ItemGroup>

<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>

</Project>
75 changes: 65 additions & 10 deletions Yafc.Parser/Data/FactorioDataDeserializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,45 @@ namespace Yafc.Parser;
internal partial class FactorioDataDeserializer {
private static readonly ILogger logger = Logging.GetLogger<FactorioDataDeserializer>();
private LuaTable raw = null!; // null-forgiving: Initialized at the beginning of LoadData.
private bool GetRef<T>(LuaTable table, string key, [NotNullWhen(true)] out T? result) where T : FactorioObject, new() {

/// <summary>
/// Gets or creates an object with the specified type (or a derived type), based on the specified value in the lua table. If the specified key
/// does not exist, this method does not get or create an object and returns <see langword="false"/>.
/// </summary>
/// <typeparam name="TReturn">The (compile-time) type of the return value.</typeparam>
/// <param name="table">The <see cref="LuaTable"/> to read to get the object's name.</param>
/// <param name="key">The table key to read to get the object's name.</param>
/// <param name="result">When successful, the new or pre-existing object described by <typeparamref name="TReturn"/> and
/// <c><paramref name="table"/>[<paramref name="key"/>]</c>. Otherwise, <see langword="null"/>.</param>
/// <returns><see langword="true"/>, if the specified key was present in the table, or <see langword="false"/> otherwise.</returns>
/// <exception cref="ArgumentException">Thrown if <c><paramref name="table"/>[<paramref name="key"/>]</c> does not describe an object in
/// <c>data.raw</c> that can be loaded as a <typeparamref name="TReturn"/>.</exception>
/// <seealso cref="GetObject{TReturn}(string)"/>
private bool GetRef<TReturn>(LuaTable table, string key, [NotNullWhen(true)] out TReturn? result) where TReturn : FactorioObject, new() {
result = null;
if (!table.Get(key, out string? name)) {
return false;
}

result = GetObject<T>(name);
result = GetObject<TReturn>(name);

return true;
}

private T? GetRef<T>(LuaTable table, string key) where T : FactorioObject, new() {
_ = GetRef<T>(table, key, out var result);
/// <summary>
/// Gets or creates an object with the specified type (or a derived type), based on the specified value in the lua table. If the specified key
/// does not exist, this method does not get or create an object and returns <see langword="null"/>.
/// </summary>
/// <typeparam name="TReturn">The (compile-time) type of the return value.</typeparam>
/// <param name="table">The <see cref="LuaTable"/> to read to get the object's name.</param>
/// <param name="key">The table key to read to get the object's name.</param>
/// <returns>If the specified key was present in the table, the new or pre-existing object described by <typeparamref name="TReturn"/> and
/// <c><paramref name="table"/>[<paramref name="key"/>]</c>. Otherwise, <see langword="null"/>.</returns>
/// <exception cref="ArgumentException">Thrown if <c><paramref name="table"/>[<paramref name="key"/>]</c> does not describe an object in
/// <c>data.raw</c> that can be loaded as a <typeparamref name="TReturn"/>.</exception>
/// <seealso cref="GetObject{TReturn}(string)"/>
private TReturn? GetRef<TReturn>(LuaTable table, string key) where TReturn : FactorioObject, new() {
_ = GetRef<TReturn>(table, key, out var result);

return result;
}
Expand Down Expand Up @@ -118,11 +144,9 @@ public Project LoadData(string projectPath, LuaTable data, LuaTable prototypes,
raw = (LuaTable?)data["raw"] ?? throw new ArgumentException("Could not load data.raw from data argument", nameof(data));
LuaTable itemPrototypes = (LuaTable?)prototypes?["item"] ?? throw new ArgumentException("Could not load prototypes.item from data argument", nameof(prototypes));

foreach (object prototypeName in Item.ExplicitPrototypeLoadOrder.Intersect(itemPrototypes.ObjectElements.Keys)) {
DeserializePrototypes(raw, (string)prototypeName, DeserializeItem, progress, errorCollector);
}
LoadPrototypes(raw, prototypes);

foreach (object prototypeName in itemPrototypes.ObjectElements.Keys.Except(Item.ExplicitPrototypeLoadOrder)) {
foreach (object prototypeName in itemPrototypes.ObjectElements.Keys) {
DeserializePrototypes(raw, (string)prototypeName, DeserializeItem, progress, errorCollector);
}

Expand Down Expand Up @@ -320,6 +344,37 @@ private unsafe Icon CreateIconFromSpec(Dictionary<(string mod, string path), Int
return IconCollection.AddIcon(targetSurface);
}

/// <summary>
/// A lookup table from (prototype, name) (e.g. ("item", "speed-module")) to subtype (e.g. "module").
/// This is used to determine the correct concrete type when constructing an item or entity by name.
/// Most values are unused, but they're all stored for simplicity and future-proofing.
/// </summary>
private readonly Dictionary<(string prototype, string name), string> prototypes = new() {
[("item", "science")] = "item",
[("item", "item-total-input")] = "item",
[("item", "item-total-output")] = "item",
};

/// <summary>
/// Load all keys (object names) from <c>data.raw[<em>type</em>]</c>, for all <em>type</em>s. Create a lookup from
/// (<em>prototype</em>, <em>name</em>) to <em>type</em>, where <c>defines.prototypes[<em>prototype</em>]</c> contains the key <em>type</em>.
/// </summary>
/// <param name="raw">The <c>data.raw</c> table.</param>
/// <param name="prototypes">The <c>defines.prototypes</c> table.</param>
/// <remarks>This stored data allows requests like "I need the 'cerys-radioactive-module-decayed' item" to correctly respond with a module,
/// regardless of where the item name is first encountered.</remarks>
private void LoadPrototypes(LuaTable raw, LuaTable prototypes) {
foreach ((object p, object? t) in prototypes.ObjectElements) {
if (p is string prototype && t is LuaTable types) { // here, 'prototype' is item, entity, equipment, etc.
foreach (string type in types.ObjectElements.Keys.Cast<string>()) { // 'type' is module, accumulator, solar-panel-equipment, etc.
foreach (string name in raw.Get<LuaTable>(type)?.ObjectElements.Keys.Cast<string>() ?? []) {
this.prototypes[(prototype, name)] = type;
}
}
}
}
}

private static void DeserializePrototypes(LuaTable data, string type, Action<LuaTable, ErrorCollector> deserializer,
IProgress<(string, string)> progress, ErrorCollector errorCollector) {

Expand Down Expand Up @@ -391,7 +446,7 @@ private static EffectReceiver ParseEffectReceiver(LuaTable? table) {

private void DeserializeItem(LuaTable table, ErrorCollector _1) {
if (table.Get("type", "") == "module" && table.Get("effect", out LuaTable? moduleEffect)) {
Module module = GetObject<Item, Module>(table);
Module module = GetObject<Module>(table);
var effect = ParseEffect(moduleEffect);
module.moduleSpecification = new ModuleSpecification {
category = table.Get("category", ""),
Expand All @@ -403,7 +458,7 @@ private void DeserializeItem(LuaTable table, ErrorCollector _1) {
};
}
else if (table.Get("type", "") == "ammo" && table["ammo_type"] is LuaTable ammo_type) {
Ammo ammo = GetObject<Item, Ammo>(table);
Ammo ammo = GetObject<Ammo>(table);
ammo_type.ReadObjectOrArray(readAmmoType);

if (ammo_type["target_filter"] is LuaTable targets) {
Expand Down
Loading