Skip to content

Commit f4ae4f2

Browse files
authored
Fix #535 and other load errors (#537)
I set out to fix #535, but then I couldn't load several of my test projects, (primarily https://mods.factorio.com/mod/kry-all-planet-mods) so I fixed those too. There's still several warnings and accessibility issues, but projects using Pacifist, PlanetsLib, Muluna, and/or Cerys (and possibly others) can at least load.
2 parents 9f6c4c4 + a2df407 commit f4ae4f2

16 files changed

+597
-105
lines changed

FactorioCalc.sln

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Yafc.I18n", "Yafc.I18n\Yafc
3030
EndProject
3131
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Yafc.I18n.Generator", "Yafc.I18n.Generator\Yafc.I18n.Generator.csproj", "{E8A28A02-99C4-41D3-99E3-E6252BD116B7}"
3232
EndProject
33+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Yafc.Parser.Tests", "Yafc.Parser.Tests\Yafc.Parser.Tests.csproj", "{E7D5C910-550A-4028-A866-C536F1FAD645}"
34+
EndProject
3335
Global
3436
GlobalSection(SolutionConfigurationPlatforms) = preSolution
3537
Debug|Any CPU = Debug|Any CPU
@@ -68,6 +70,10 @@ Global
6870
{E8A28A02-99C4-41D3-99E3-E6252BD116B7}.Debug|Any CPU.Build.0 = Debug|Any CPU
6971
{E8A28A02-99C4-41D3-99E3-E6252BD116B7}.Release|Any CPU.ActiveCfg = Release|Any CPU
7072
{E8A28A02-99C4-41D3-99E3-E6252BD116B7}.Release|Any CPU.Build.0 = Release|Any CPU
73+
{E7D5C910-550A-4028-A866-C536F1FAD645}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
74+
{E7D5C910-550A-4028-A866-C536F1FAD645}.Debug|Any CPU.Build.0 = Debug|Any CPU
75+
{E7D5C910-550A-4028-A866-C536F1FAD645}.Release|Any CPU.ActiveCfg = Release|Any CPU
76+
{E7D5C910-550A-4028-A866-C536F1FAD645}.Release|Any CPU.Build.0 = Release|Any CPU
7177
EndGlobalSection
7278
GlobalSection(SolutionProperties) = preSolution
7379
HideSolutionNode = FALSE

Yafc.Model.Tests/LuaDependentTestHelper.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ internal static Project GetProjectForLua(string targetStreamName = null) {
6363
// Read the four lua files and generate an empty project.
6464
Project project;
6565
Helper helper = new();
66-
using (LuaContext context = new LuaContext()) {
66+
using (LuaContext context = new LuaContext(new(2, 0, 7))) {
6767
byte[] bytes = File.ReadAllBytes("Data/Sandbox.lua");
6868
context.Exec(bytes, "*", "");
6969

Yafc.Model/Data/DataClasses.cs

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -383,16 +383,6 @@ public Item() {
383383
Recipe? getSpoilRecipe() => Database.recipes.all.OfType<Mechanics>().SingleOrDefault(r => r.name == "spoil." + name);
384384
}
385385

386-
/// <summary>
387-
/// The prototypes in this array will be loaded in order, before any other prototypes.
388-
/// This should correspond to the prototypes for subclasses of item, with more derived classes listed before their base classes.
389-
/// e.g. If ItemWithHealth and ModuleWithHealth C# classes were added, all prototypes for both classes must be listed here.
390-
/// Ignoring style restrictions, the prototypes could be listed in any order, provided all ModuleWithHealth prototype(s) are listed before "module".
391-
/// </summary>
392-
/// <remarks>This forces modules to be loaded before other items, since deserialization otherwise creates Item objects for all spoil results.
393-
/// It does not protect against modules that spoil into other modules, but one hopes people won't do that.</remarks>
394-
internal static string[] ExplicitPrototypeLoadOrder { get; } = ["ammo", "module"];
395-
396386
public Item? fuelResult { get; internal set; }
397387
public int stackSize { get; internal set; }
398388
public Entity? placeResult { get; internal set; }
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
using static Yafc.Parser.MathExpression;
2+
3+
namespace Yafc.Parser.Tests;
4+
5+
public class MathExpressionTests {
6+
// Note that these tests are cumulative. If there are test failures, fix tokenization first, transpilation second, and evaluation last.
7+
[Theory]
8+
[MemberData(nameof(TokenizationData))]
9+
public void TestTokenization(string input, List<object?> output)
10+
=> Assert.Equivalent(output, Tokenize(input));
11+
12+
[Theory]
13+
[MemberData(nameof(TranspilationData))]
14+
public void TestTranspilation(string input, string? output)
15+
=> Assert.Equal(output, Transpile(Tokenize(input)));
16+
17+
[Theory]
18+
[MemberData(nameof(EvaluationData))]
19+
public void TestEvaluation(string input, float output)
20+
=> Assert.Equal(output, Evaluate(input, null));
21+
22+
public static TheoryData<string, List<object?>> TokenizationData => new() {
23+
{ "a * b", ["a", Token.Asterisk, "b"] },
24+
{ "call(x, y)", ["call", Token.OpenParen, "x", Token.Comma, "y", Token.CloseParen] },
25+
{ "x^y^z", ["x", Token.Caret, "y", Token.Caret, "z"] },
26+
{ "0x1234 + 123.456", [4660f, Token.Plus, 123.456f] },
27+
{ "log2(5)", ["log2", Token.OpenParen, 5f, Token.CloseParen] },
28+
{ "$", [null] },
29+
};
30+
31+
public static TheoryData<string, string?> TranspilationData => new() {
32+
{ "a * b", "@a*@b;" },
33+
{ "call(x, y)", "@call(@x,@y);" },
34+
{ "x^y^z", "@x..@y..@z;" },
35+
{ "0x1234 + 123.456", "4660+123.456;" },
36+
{ "log2(5)", "@log2(5);" },
37+
{ "$", null },
38+
};
39+
40+
public static TheoryData<string, float> EvaluationData => new() {
41+
{ "1+2*3", 7 },
42+
{ "2^3^2", 512 },
43+
{ "2^(2^3)", 256 },
44+
{ "(2^3)^2", 64 },
45+
{ "0x1234 * 123.456", 0x1234 * 123.456f },
46+
{ "log2(5)", MathF.Log2(5) },
47+
};
48+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net8.0</TargetFramework>
5+
<ImplicitUsings>enable</ImplicitUsings>
6+
<Nullable>enable</Nullable>
7+
<IsTestProject>true</IsTestProject>
8+
</PropertyGroup>
9+
10+
<ItemGroup>
11+
<PackageReference Include="coverlet.collector" Version="6.0.0" />
12+
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
13+
<PackageReference Include="xunit" Version="2.5.3" />
14+
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.3" />
15+
</ItemGroup>
16+
17+
<ItemGroup>
18+
<ProjectReference Include="..\Yafc.Parser\Yafc.Parser.csproj" />
19+
</ItemGroup>
20+
21+
<ItemGroup>
22+
<Using Include="Xunit" />
23+
</ItemGroup>
24+
25+
</Project>

Yafc.Parser/Data/FactorioDataDeserializer.cs

Lines changed: 65 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,19 +15,45 @@ namespace Yafc.Parser;
1515
internal partial class FactorioDataDeserializer {
1616
private static readonly ILogger logger = Logging.GetLogger<FactorioDataDeserializer>();
1717
private LuaTable raw = null!; // null-forgiving: Initialized at the beginning of LoadData.
18-
private bool GetRef<T>(LuaTable table, string key, [NotNullWhen(true)] out T? result) where T : FactorioObject, new() {
18+
19+
/// <summary>
20+
/// 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
21+
/// does not exist, this method does not get or create an object and returns <see langword="false"/>.
22+
/// </summary>
23+
/// <typeparam name="TReturn">The (compile-time) type of the return value.</typeparam>
24+
/// <param name="table">The <see cref="LuaTable"/> to read to get the object's name.</param>
25+
/// <param name="key">The table key to read to get the object's name.</param>
26+
/// <param name="result">When successful, the new or pre-existing object described by <typeparamref name="TReturn"/> and
27+
/// <c><paramref name="table"/>[<paramref name="key"/>]</c>. Otherwise, <see langword="null"/>.</param>
28+
/// <returns><see langword="true"/>, if the specified key was present in the table, or <see langword="false"/> otherwise.</returns>
29+
/// <exception cref="ArgumentException">Thrown if <c><paramref name="table"/>[<paramref name="key"/>]</c> does not describe an object in
30+
/// <c>data.raw</c> that can be loaded as a <typeparamref name="TReturn"/>.</exception>
31+
/// <seealso cref="GetObject{TReturn}(string)"/>
32+
private bool GetRef<TReturn>(LuaTable table, string key, [NotNullWhen(true)] out TReturn? result) where TReturn : FactorioObject, new() {
1933
result = null;
2034
if (!table.Get(key, out string? name)) {
2135
return false;
2236
}
2337

24-
result = GetObject<T>(name);
38+
result = GetObject<TReturn>(name);
2539

2640
return true;
2741
}
2842

29-
private T? GetRef<T>(LuaTable table, string key) where T : FactorioObject, new() {
30-
_ = GetRef<T>(table, key, out var result);
43+
/// <summary>
44+
/// 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
45+
/// does not exist, this method does not get or create an object and returns <see langword="null"/>.
46+
/// </summary>
47+
/// <typeparam name="TReturn">The (compile-time) type of the return value.</typeparam>
48+
/// <param name="table">The <see cref="LuaTable"/> to read to get the object's name.</param>
49+
/// <param name="key">The table key to read to get the object's name.</param>
50+
/// <returns>If the specified key was present in the table, the new or pre-existing object described by <typeparamref name="TReturn"/> and
51+
/// <c><paramref name="table"/>[<paramref name="key"/>]</c>. Otherwise, <see langword="null"/>.</returns>
52+
/// <exception cref="ArgumentException">Thrown if <c><paramref name="table"/>[<paramref name="key"/>]</c> does not describe an object in
53+
/// <c>data.raw</c> that can be loaded as a <typeparamref name="TReturn"/>.</exception>
54+
/// <seealso cref="GetObject{TReturn}(string)"/>
55+
private TReturn? GetRef<TReturn>(LuaTable table, string key) where TReturn : FactorioObject, new() {
56+
_ = GetRef<TReturn>(table, key, out var result);
3157

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

121-
foreach (object prototypeName in Item.ExplicitPrototypeLoadOrder.Intersect(itemPrototypes.ObjectElements.Keys)) {
122-
DeserializePrototypes(raw, (string)prototypeName, DeserializeItem, progress, errorCollector);
123-
}
147+
LoadPrototypes(raw, prototypes);
124148

125-
foreach (object prototypeName in itemPrototypes.ObjectElements.Keys.Except(Item.ExplicitPrototypeLoadOrder)) {
149+
foreach (object prototypeName in itemPrototypes.ObjectElements.Keys) {
126150
DeserializePrototypes(raw, (string)prototypeName, DeserializeItem, progress, errorCollector);
127151
}
128152

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

347+
/// <summary>
348+
/// A lookup table from (prototype, name) (e.g. ("item", "speed-module")) to subtype (e.g. "module").
349+
/// This is used to determine the correct concrete type when constructing an item or entity by name.
350+
/// Most values are unused, but they're all stored for simplicity and future-proofing.
351+
/// </summary>
352+
private readonly Dictionary<(string prototype, string name), string> prototypes = new() {
353+
[("item", "science")] = "item",
354+
[("item", "item-total-input")] = "item",
355+
[("item", "item-total-output")] = "item",
356+
};
357+
358+
/// <summary>
359+
/// Load all keys (object names) from <c>data.raw[<em>type</em>]</c>, for all <em>type</em>s. Create a lookup from
360+
/// (<em>prototype</em>, <em>name</em>) to <em>type</em>, where <c>defines.prototypes[<em>prototype</em>]</c> contains the key <em>type</em>.
361+
/// </summary>
362+
/// <param name="raw">The <c>data.raw</c> table.</param>
363+
/// <param name="prototypes">The <c>defines.prototypes</c> table.</param>
364+
/// <remarks>This stored data allows requests like "I need the 'cerys-radioactive-module-decayed' item" to correctly respond with a module,
365+
/// regardless of where the item name is first encountered.</remarks>
366+
private void LoadPrototypes(LuaTable raw, LuaTable prototypes) {
367+
foreach ((object p, object? t) in prototypes.ObjectElements) {
368+
if (p is string prototype && t is LuaTable types) { // here, 'prototype' is item, entity, equipment, etc.
369+
foreach (string type in types.ObjectElements.Keys.Cast<string>()) { // 'type' is module, accumulator, solar-panel-equipment, etc.
370+
foreach (string name in raw.Get<LuaTable>(type)?.ObjectElements.Keys.Cast<string>() ?? []) {
371+
this.prototypes[(prototype, name)] = type;
372+
}
373+
}
374+
}
375+
}
376+
}
377+
323378
private static void DeserializePrototypes(LuaTable data, string type, Action<LuaTable, ErrorCollector> deserializer,
324379
IProgress<(string, string)> progress, ErrorCollector errorCollector) {
325380

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

392447
private void DeserializeItem(LuaTable table, ErrorCollector _1) {
393448
if (table.Get("type", "") == "module" && table.Get("effect", out LuaTable? moduleEffect)) {
394-
Module module = GetObject<Item, Module>(table);
449+
Module module = GetObject<Module>(table);
395450
var effect = ParseEffect(moduleEffect);
396451
module.moduleSpecification = new ModuleSpecification {
397452
category = table.Get("category", ""),
@@ -403,7 +458,7 @@ private void DeserializeItem(LuaTable table, ErrorCollector _1) {
403458
};
404459
}
405460
else if (table.Get("type", "") == "ammo" && table["ammo_type"] is LuaTable ammo_type) {
406-
Ammo ammo = GetObject<Item, Ammo>(table);
461+
Ammo ammo = GetObject<Ammo>(table);
407462
ammo_type.ReadObjectOrArray(readAmmoType);
408463

409464
if (ammo_type["target_filter"] is LuaTable targets) {

0 commit comments

Comments
 (0)