Skip to content
Open
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
4 changes: 2 additions & 2 deletions Yafc.Model.Tests/Model/ProductionTableContentTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ public async Task AllCombinationsWithVariousFixedCounts_DisplayedProductsMatchSc

await table.Solve(page);

foreach (var (display, solver) in row.Ingredients.Zip(((IRecipeRow)row).IngredientsForSolver)) {
foreach (var (display, solver) in row.Ingredients.Zip(row.IngredientsForSolver)) {
var (solverGoods, solverAmount, _, _, _) = solver;
var (displayGoods, displayAmount, _, _) = display;

Expand All @@ -154,7 +154,7 @@ public async Task AllCombinationsWithVariousFixedCounts_DisplayedProductsMatchSc
}
}

foreach (var (display, solver) in row.Products.Zip(((IRecipeRow)row).ProductsForSolver
foreach (var (display, solver) in row.Products.Zip(row.ProductsForSolver
// ProductsForSolver doesn't include the spent fuel. Append an entry for the spent fuel, in the case that the spent
// fuel is not a recipe product.
// If the spent fuel is also a recipe product, this value will ignored in favor of the recipe-product value.
Expand Down
23 changes: 23 additions & 0 deletions Yafc.Model.Tests/Model/ProductionTableTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -111,4 +111,27 @@ public void ProductionTableTest_CanSaveAndLoadWithEachPageContentType(Type conte

public static TheoryData<Type> ProjectPageContentTypes => [.. AppDomain.CurrentDomain.GetAssemblies().SelectMany(a => a.GetTypes())
.Where(t => typeof(ProjectPageContents).IsAssignableFrom(t) && !t.IsAbstract)];

[Fact]
public void ProductionTableTest_CanLoadWithNullRecipe() {
Project project = LuaDependentTestHelper.GetProjectForLua("Yafc.Model.Tests.Model.ProductionTableContentTests.lua");

ProjectPage page = new(project, typeof(ProductionTable));
project.pages.Add(page);
ProductionTable table = (ProductionTable)page.content;
RecipeRow nullRecipe = new RecipeRow(table, null);
table.recipes.Add(nullRecipe);
nullRecipe.subgroup = new ProductionTable(nullRecipe);
nullRecipe.subgroup.AddRecipe(Database.recipes.all.Single(r => r.name == "recipe").With(Quality.Normal), DataUtils.DeterministicComparer);

ErrorCollector collector = new();
using MemoryStream stream = new();
project.Save(stream);
Project newProject = Project.Read(stream.ToArray(), collector);

Assert.Equal(ErrorSeverity.None, collector.severity);
Assert.Equal(project.pages.Select(s => s.guid), newProject.pages.Select(p => p.guid));
Assert.Equal(((ProductionTable)project.pages[0].content).GetAllRecipes().Select(r => r.recipe),
((ProductionTable)newProject.pages[0].content).GetAllRecipes().Select(r => r.recipe));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ public class SerializationTreeChangeDetection {
},
[typeof(RecipeRow)] = new() {
[nameof(RecipeRow.recipe)] = typeof(IObjectWithQuality<RecipeOrTechnology>),
[nameof(RecipeRow.icon)] = typeof(FactorioObject),
[nameof(RecipeRow.description)] = typeof(string),
[nameof(RecipeRow.entity)] = typeof(IObjectWithQuality<EntityCrafter>),
[nameof(RecipeRow.fuel)] = typeof(IObjectWithQuality<Goods>),
[nameof(RecipeRow.fixedBuildings)] = typeof(float),
Expand Down
4 changes: 2 additions & 2 deletions Yafc.Model/Blueprints/BlueprintUtilities.cs
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ private class LayoutResult {
public static string ExportRecipiesAsBlueprint(string name, IEnumerable<RecipeRow> recipies, bool includeFuel, bool copyToClipboard = true) {
// Sort buildings largest to smallest (by height then width) for better packing
var entities = recipies
.Where(r => r.entity is not null)
.Where(r => r.entity is not null && r.recipe is not null)
.OrderByDescending(r => r.entity!.target.size)
.ToList();

Expand Down Expand Up @@ -166,7 +166,7 @@ public static string ExportRecipiesAsBlueprint(string name, IEnumerable<RecipeRo
};

if (!recipe.recipe.Is<Mechanics>()) {
entity.recipe = recipe.recipe.target.name;
entity.recipe = recipe.recipe!.target.name;
entity.recipe_quality = recipe.recipe.quality.name;
}

Expand Down
4 changes: 4 additions & 0 deletions Yafc.Model/Model/ModuleFillerParameters.cs
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,8 @@ internal void AutoFillBeacons(RecipeOrTechnology recipe, EntityCrafter entity, r
private void AutoFillModules((float recipeTime, float fuelUsagePerSecondPerBuilding) partialParams, RecipeRow row,
EntityCrafter entity, ref ModuleEffects effects, ref UsedModule used) {

if (row.recipe == null) { return; }

Quality quality = Quality.MaxAccessible;

IObjectWithQuality<RecipeOrTechnology> recipe = row.recipe;
Expand Down Expand Up @@ -194,6 +196,8 @@ The payback time is calculated as the module cost divided by the economy gain pe
}

internal void GetModulesInfo((float recipeTime, float fuelUsagePerSecondPerBuilding) partialParams, RecipeRow row, EntityCrafter entity, ref ModuleEffects effects, ref UsedModule used) {
if (row.recipe == null) { return; }

AutoFillBeacons(row.recipe.target, entity, ref effects, ref used);
AutoFillModules(partialParams, row, entity, ref effects, ref used);
}
Expand Down
22 changes: 9 additions & 13 deletions Yafc.Model/Model/ProductionTable.GenuineRecipe.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ public partial class ProductionTable {
/// <param name="extraLinks">The <see cref="Dictionary{TKey, TValue}"/> that will eventually the contain extra links this recipe needs to
/// consider. This may be incomplete at the time of construction, provided it is complete before the first call to <see cref="FindLink"/>.
/// </param>
private class GenuineRecipe(RecipeRow row, Dictionary<(ProductionTable, IObjectWithQuality<Goods>), IProductionLink> extraLinks) : IRecipeRow {
private class GenuineRecipe(RecipeRow row, Dictionary<(ProductionTable, IObjectWithQuality<Goods>), IProductionLink> extraLinks) : ISolverRow {
/// <summary>
/// Check both <see cref="row"/> and <see cref="extraLinks"/> for a link corresponding to <paramref name="goods"/>.
/// </summary>
Expand All @@ -35,21 +35,17 @@ public bool FindLink(IObjectWithQuality<Goods> goods, [MaybeNullWhen(false)] out
}

// Pass all remaining calls through to the underlying RecipeRow.
public IObjectWithQuality<EntityCrafter>? entity => row.entity;
public IObjectWithQuality<Goods>? fuel => row.fuel;
public float fixedBuildings => row.fixedBuildings;
public double recipesPerSecond { get => row.recipesPerSecond; set => row.recipesPerSecond = value; }
public float RecipeTime => ((IRecipeRow)row).RecipeTime;
public RecipeParameters parameters { get => row.parameters; set => row.parameters = value; }
public RecipeParameters parameters => row.parameters;
public double recipesPerSecond { set => row.recipesPerSecond = value; }
public RecipeLinks links => row.links;
public IEnumerable<SolverIngredient> IngredientsForSolver => ((IRecipeRow)row).IngredientsForSolver;
public IEnumerable<SolverProduct> ProductsForSolver => ((IRecipeRow)row).ProductsForSolver;
public string SolverName => ((IRecipeRow)row).SolverName;
public double BaseCost => ((IRecipeRow)row).BaseCost;
public IEnumerable<SolverIngredient> IngredientsForSolver => row.IngredientsForSolver;
public IEnumerable<SolverProduct> ProductsForSolver => row.ProductsForSolver;
// null-forgiving: GenuineRecipes aren't constructed from non-recipe rows.
public string SolverName => row.recipe!.QualityName();
public double BaseCost => (row.recipe!.target as Recipe)?.RecipeBaseCost() ?? 0;

public void GetModulesInfo((float recipeTime, float fuelUsagePerSecondPerBuilding) recipeParams, EntityCrafter entity, ref ModuleEffects effects, ref UsedModule used)
=> row.GetModulesInfo(recipeParams, entity, ref effects, ref used);

RecipeRow? IRecipeRow.RecipeRow => row;
RecipeRow? ISolverRow.RecipeRow => row;
}
}
4 changes: 2 additions & 2 deletions Yafc.Model/Model/ProductionTable.ImplicitLink.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@ private class ImplicitLink(IObjectWithQuality<Goods> goods, ProductionTable owne
/// </summary>
public float amount => 0;

public HashSet<IRecipeRow> capturedRecipes { get; } = [];
public HashSet<ISolverRow> capturedRecipes { get; } = [];
public int solverIndex { get; set; }
public float linkFlow { get; set; }
public float linkFlow { set { } }
public float notMatchedFlow { get; set; }

public ProductionTable owner { get; } = owner;
Expand Down
20 changes: 5 additions & 15 deletions Yafc.Model/Model/ProductionTable.ScienceDecomposition.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using System;
using System.Collections.Generic;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;

namespace Yafc.Model;
Expand All @@ -12,9 +11,7 @@ public partial class ProductionTable {
/// <param name="quality">The quality of the recipe's input pack.</param>
/// <param name="productLink">The (genuine) link for the output pack.</param>
/// <param name="ingredientLink">The (implicit) link for the input pack.</param>
private class ScienceDecomposition(Goods pack, Quality quality, IProductionLink productLink, ImplicitLink ingredientLink) : IRecipeRow {
public IObjectWithQuality<EntityCrafter>? entity => null;

private class ScienceDecomposition(Goods pack, Quality quality, IProductionLink productLink, ImplicitLink ingredientLink) : ISolverRow {
public IObjectWithQuality<Goods>? fuel => null;

/// <summary>
Expand All @@ -27,22 +24,15 @@ private class ScienceDecomposition(Goods pack, Quality quality, IProductionLink
/// <inheritdoc/>
public IEnumerable<SolverProduct> ProductsForSolver => [new SolverProduct(pack.With(Quality.Normal), quality.level + 1, productLink, 0, null)];

public double recipesPerSecond { get; set; }
public RecipeParameters parameters { get; set; } = RecipeParameters.Empty;
public RecipeParameters parameters { get; } = RecipeParameters.Empty;
public double recipesPerSecond { set { } }
public RecipeLinks links => new() { ingredients = [null] };

/// <summary>
/// Always 1; the solver needs something non-zero to calculate <see cref="fixedBuildings"/>.
/// </summary>
public float RecipeTime => 1;

/// <inheritdoc/>
public string SolverName => $"Convert {pack.name} from {quality.name}";

public double BaseCost => 0;

public void GetModulesInfo((float recipeTime, float fuelUsagePerSecondPerBuilding) recipeParams, EntityCrafter entity, ref ModuleEffects effects, ref UsedModule used) => throw new NotImplementedException();

public bool FindLink(IObjectWithQuality<Goods> goods, [MaybeNullWhen(false)] out IProductionLink link) {
if (ingredientLink.goods == goods) {
link = ingredientLink;
Expand All @@ -59,6 +49,6 @@ public bool FindLink(IObjectWithQuality<Goods> goods, [MaybeNullWhen(false)] out
/// <summary>
/// Always null; this does not represent a user-visible <see cref="RecipeRow"/>.
/// </summary>
RecipeRow? IRecipeRow.RecipeRow => null;
RecipeRow? ISolverRow.RecipeRow => null;
}
}
29 changes: 14 additions & 15 deletions Yafc.Model/Model/ProductionTable.cs
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ public void RebuildLinkMap() {
/// <returns>A tuple containing (1) the normal science packs links for science recipes at this or a deeper level, and (2) the quality science
/// packs produced at this or a deeper level, but not linked.</returns>
private (Dictionary<Goods, IProductionLink> linkedConsumption, HashSet<IObjectWithQuality<Goods>> unlinkedProduction)
Setup(List<IRecipeRow> allRecipes, List<IProductionLink> allLinks,
Setup(List<ISolverRow> allRecipes, List<IProductionLink> allLinks,
Dictionary<(ProductionTable, IObjectWithQuality<Goods>), IProductionLink> extraLinks) {

containsDesiredProducts = false;
Expand Down Expand Up @@ -143,9 +143,8 @@ public void RebuildLinkMap() {
}
unlinkedProduction.UnionWith(nestedProduction);
}
else {
else if (recipe.recipe != null) {
// The header recipe for this table, and all recipes that are not part of a nested table.
recipe.parameters = RecipeParameters.CalculateParameters(recipe);
allRecipes.Add(new GenuineRecipe(recipe, extraLinks));

if (recipe.recipe.Is<Technology>()) {
Expand Down Expand Up @@ -427,8 +426,8 @@ bool shouldBeReported(ProductionTableFlow flow) {
}

HashSet<RecipeRow> recipes = [.. GetAllRecipes(), parent];
foreach (IRecipeRow iRec in link.capturedRecipes) {
if (iRec is not RecipeRow recipe || (!recipes.Contains(recipe) && recipe.DetermineFlow(flow.goods) != 0)) {
foreach (ISolverRow row in link.capturedRecipes) {
if (row.RecipeRow is not RecipeRow recipe || (!recipes.Contains(recipe) && recipe.DetermineFlow(flow.goods) != 0)) {
return true;
}
}
Expand All @@ -440,20 +439,18 @@ bool shouldBeReported(ProductionTableFlow flow) {
/// Add/update the variable value for the constraint with the given amount, and store the recipe to the production link.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void AddLinkCoefficient(Constraint cst, Variable var, IProductionLink link, IRecipeRow recipe, float amount) {
private static void AddLinkCoefficient(Constraint cst, Variable var, IProductionLink link, ISolverRow recipe, float amount) {
// GetCoefficient will return 0 when the variable is not available in the constraint
amount += (float)cst.GetCoefficient(var);
// To avoid false negatives when testing "iRecipeRow is RecipeRow" or otherwise inspecting the content of capturedRecipes,
// store the underlying RecipeRow if available.
_ = link.capturedRecipes.Add(recipe.RecipeRow ?? recipe);
_ = link.capturedRecipes.Add(recipe);
cst.SetCoefficient(var, amount);
}

public override async Task<string?> Solve(ProjectPage page) {
using var productionTableSolver = DataUtils.CreateSolver();
var objective = productionTableSolver.Objective();
objective.SetMinimization();
List<IRecipeRow> allRecipes = [];
List<ISolverRow> allRecipes = [];
List<IProductionLink> allLinks = [];
Setup(allRecipes, allLinks, []);
Variable[] vars = new Variable[allRecipes.Count];
Expand Down Expand Up @@ -690,7 +687,9 @@ private static void AddLinkCoefficient(Constraint cst, Variable var, IProduction
/// <returns><see langword="true"/> if the link should be preserved, or <see langword="false"/> if it is ok to delete the link.</returns>
private bool HasDisabledRecipeReferencing(IObjectWithQuality<Goods> goods)
=> GetAllRecipes().Any(row => !row.hierarchyEnabled
&& (row.fuel == goods || row.recipe.target.ingredients.Any(i => i.goods == goods) || row.recipe.target.products.Any(p => p.goods == goods)));
&& (row.fuel == goods
|| (row.recipe?.target.ingredients.Any(i => i.goods == goods.target) ?? false)
|| (row.recipe?.target.products.Any(p => p.goods == goods.target) ?? false)));

private bool CheckBuiltCountExceeded() {
bool builtCountExceeded = false;
Expand All @@ -713,7 +712,7 @@ private bool CheckBuiltCountExceeded() {
return builtCountExceeded;
}

private static void FindAllRecipeLinks(IRecipeRow recipe, List<IProductionLink> sources, List<IProductionLink> targets) {
private static void FindAllRecipeLinks(ISolverRow recipe, List<IProductionLink> sources, List<IProductionLink> targets) {
sources.Clear();
targets.Clear();

Expand All @@ -738,7 +737,7 @@ private static void FindAllRecipeLinks(IRecipeRow recipe, List<IProductionLink>
}
}

private static (List<IProductionLink> merges, List<IProductionLink> splits) GetInfeasibilityCandidates(List<IRecipeRow> recipes) {
private static (List<IProductionLink> merges, List<IProductionLink> splits) GetInfeasibilityCandidates(List<ISolverRow> recipes) {
Graph<IProductionLink> graph = new Graph<IProductionLink>();
List<IProductionLink> sources = [];
List<IProductionLink> targets = [];
Expand Down Expand Up @@ -828,7 +827,7 @@ public bool Contains(RecipeOrTechnology obj) {
if (owner is RecipeRow { recipe.target: RecipeOrTechnology recipe } && recipe == obj) {
return true;
}
return recipes.Any(r => r.recipe.target == obj);
return recipes.Any(r => r.recipe?.target == obj);
}

/// <summary>
Expand All @@ -840,7 +839,7 @@ public bool Contains(RecipeOrTechnology obj) {
/// Returns <see langword="true"/> if the specified recipe appears at any quality anywhere on this table's <see cref="ProjectPage"/>.
/// </summary>
/// <remarks>This is most commonly used for deciding whether to draw a yellow checkmark.</remarks>
public bool ContainsAnywhere(RecipeOrTechnology obj) => rootTable.GetAllRecipes().Any(r => r.recipe.target == obj);
public bool ContainsAnywhere(RecipeOrTechnology obj) => rootTable.GetAllRecipes().Any(r => r.recipe?.target == obj);

public bool CreateLink(IObjectWithQuality<Goods> goods) {
if (linkMap.GetValueOrDefault(goods) is ProductionLink || !goods.target.isLinkable) {
Expand Down
Loading