Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Better barrelling and stacking detection #134

Merged
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
7 changes: 4 additions & 3 deletions Yafc.Model/Data/DataClasses.cs
Original file line number Diff line number Diff line change
Expand Up @@ -144,10 +144,11 @@ public bool CanFit(int itemInputs, int fluidInputs, Goods[]? slots) {

public enum FactorioObjectSpecialType {
Normal,
FilledBarrel,
Barreling,
Voiding,
Unbarreling
Barreling,
Stacking,
Pressurization,
Crating,
}

public class Recipe : RecipeOrTechnology {
Expand Down
140 changes: 115 additions & 25 deletions Yafc.Parser/Data/FactorioDataDeserializer_Context.cs
Original file line number Diff line number Diff line change
Expand Up @@ -154,43 +154,89 @@ private void ExportBuiltData() {
Database.allContainers = Database.entities.all.OfType<EntityContainer>().ToArray();
}

private bool IsBarrelingRecipe(Recipe barreling, Recipe unbarreling) {
var product = barreling.products[0];
if (product.probability != 1f) {
private static bool AreInverseRecipes(Recipe packing, Recipe unpacking) {
var packedProduct = packing.products[0];

// Check for deterministic production
if (packedProduct.probability != 1f || unpacking.products.Any(p => p.probability != 1)) {
return false;
}

if (product.goods is not Item barrel) {
if (packedProduct.amountMin != packedProduct.amountMax || unpacking.products.Any(p => p.amountMin != p.amountMax)) {
return false;
}

if (unbarreling.ingredients.Length != 1) {
if (unpacking.ingredients.Length != 1 || packing.ingredients.Length != unpacking.products.Length) {
return false;
}

var ingredient = unbarreling.ingredients[0];
if (ingredient.variants != null || ingredient.goods != barrel || ingredient.amount != product.amount) {
// Check for 'packing.ingredients == unpacking.products'.
float ratio = 0;
Recipe? largerRecipe = null;

// Check for 'packing.ingredients == unpacking.products'.
if (!checkRatios(packing, unpacking, ref ratio, ref largerRecipe)) {
return false;
}

if (unbarreling.products.Length != barreling.ingredients.Length) {
// Check for 'unpacking.ingredients == packing.products'.
if (!checkRatios(unpacking, packing, ref ratio, ref largerRecipe)) {
return false;
}

if (barrel.miscSources.Length != 0 || barrel.fuelValue != 0f || barrel.placeResult != null || barrel is Module { moduleSpecification: not null }) {
return false;
return true;


// Test to see if running `first` M times and `second` once, or vice versa, can reproduce all the original input.
// Track which recipe is larger to keep ratio an integer and prevent floating point rounding issues.
static bool checkRatios(Recipe first, Recipe second, ref float ratio, ref Recipe? larger) {
Dictionary<Goods, float> ingredients = [];

foreach (var item in first.ingredients) {
if (ingredients.ContainsKey(item.goods)) {
return false; // Refuse to deal with duplicate ingredients.
}
ingredients[item.goods] = item.amount;
}

foreach (var item in second.products) {
if (!ingredients.TryGetValue(item.goods, out float count)) {
return false;
}
if (count > item.amount) {
if (!checkProportions(first, count, item.amount, ref ratio, ref larger)) {
return false;
}
}
else if (count == item.amount) {
if (ratio != 0 && ratio != 1) {
return false;
}
ratio = 1;
}
else {
if (!checkProportions(second, item.amount, count, ref ratio, ref larger)) {
return false;
}
}
}
return true;
}

foreach (var (testProduct, testIngredient) in unbarreling.products.Zip(barreling.ingredients)) {
if (testProduct.probability != 1f || testProduct.goods != testIngredient.goods || testIngredient.variants != null || testProduct.amount != testIngredient.amount) {
// Within the previous check, make sure the ratio is an integer.
// If the ratio was set by a previous ingredient/product Goods, make sure this ratio matches the previous one.
static bool checkProportions(Recipe currentLargerRecipe, float largerCount, float smallerCount, ref float ratio, ref Recipe? larger) {
if (largerCount / smallerCount != MathF.Floor(largerCount / smallerCount)) {
return false;
}
if (ratio != 0 && ratio != largerCount / smallerCount) {
return false;
}
if (larger != null && larger != currentLargerRecipe) {
return false;
}
ratio = largerCount / smallerCount;
larger = currentLargerRecipe;
return true;
}
if (unbarreling.IsProductivityAllowed() || barreling.IsProductivityAllowed()) {
return false;
}

return true;
}

/// <summary>
Expand Down Expand Up @@ -346,7 +392,7 @@ private void CalculateMaps(bool netProduction) {
mechanic.iconSpec = mechanic.source.iconSpec;
}

// step 3 - detect barreling/unbarreling and voiding recipes
// step 3 - detect packing/unpacking (e.g. barreling/unbarreling, stacking/unstacking, etc.) and voiding recipes
foreach (var recipe in allRecipes) {
if (recipe.specialType != FactorioObjectSpecialType.Normal) {
continue;
Expand All @@ -360,12 +406,52 @@ private void CalculateMaps(bool netProduction) {
continue;
}

if (recipe.products[0].goods is Item barrel) {
foreach (var usage in barrel.usages) {
if (IsBarrelingRecipe(recipe, usage)) {
Goods packed = recipe.products[0].goods;
if (countNonDsrRecipes(packed.usages) != 1 && countNonDsrRecipes(packed.production) != 1) {
continue;
}

if (recipe.ingredients.Sum(i => i.amount) <= recipe.products.Sum(p => p.amount)) {
// If `recipe` is part of packing/unpacking pair, it's the unpacking half. Ignore it until we find the packing half of the pair.
continue;
}

foreach (var unpacking in packed.usages) {
if (AreInverseRecipes(recipe, unpacking)) {
if (packed is Fluid && unpacking.products.All(p => p.goods is Fluid)) {
recipe.specialType = FactorioObjectSpecialType.Pressurization;
unpacking.specialType = FactorioObjectSpecialType.Pressurization;
packed.specialType = FactorioObjectSpecialType.Pressurization;
}
else if (packed is Item && unpacking.products.All(p => p.goods is Item)) {
if (unpacking.products.Length == 1) {
recipe.specialType = FactorioObjectSpecialType.Stacking;
unpacking.specialType = FactorioObjectSpecialType.Stacking;
packed.specialType = FactorioObjectSpecialType.Stacking;
}
else {
recipe.specialType = FactorioObjectSpecialType.Crating;
unpacking.specialType = FactorioObjectSpecialType.Crating;
packed.specialType = FactorioObjectSpecialType.Crating;
}
}
else if (packed is Item && unpacking.products.Any(p => p.goods is Item) && unpacking.products.Any(p => p.goods is Fluid)) {
recipe.specialType = FactorioObjectSpecialType.Barreling;
usage.specialType = FactorioObjectSpecialType.Unbarreling;
barrel.specialType = FactorioObjectSpecialType.FilledBarrel;
unpacking.specialType = FactorioObjectSpecialType.Barreling;
packed.specialType = FactorioObjectSpecialType.Barreling;
}
else { continue; }

// The packed good is used in other recipes or is fuel, constructs a building, or is a module. Only the unpacking recipe should be flagged as special.
if (countNonDsrRecipes(packed.usages) != 1 || (packed is Item item && (item.fuelValue != 0 || item.placeResult != null || item is Module))) {
recipe.specialType = FactorioObjectSpecialType.Normal;
packed.specialType = FactorioObjectSpecialType.Normal;
}

// The packed good can be mined or has a non-packing source. Only the packing recipe should be flagged as special.
if (packed.miscSources.OfType<Entity>().Any() || countNonDsrRecipes(packed.production) > 1) {
unpacking.specialType = FactorioObjectSpecialType.Normal;
packed.specialType = FactorioObjectSpecialType.Normal;
}
}
}
Expand All @@ -380,6 +466,10 @@ private void CalculateMaps(bool netProduction) {
fluid.locName += " " + fluid.temperature + "°";
}
}
// The recipes added by deadlock_stacked_recipes (with CompressedFluids, if present) need to be filtered out to get decent results.
static int countNonDsrRecipes(IEnumerable<Recipe> recipes) {
return recipes.Count(r => !r.name.Contains("StackedRecipe-") && !r.name.Contains("DSR_HighPressure-"));
}
}

private Recipe CreateSpecialRecipe(FactorioObject production, string category, string hint) {
Expand Down
6 changes: 6 additions & 0 deletions changelog.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
----------------------------------------------------------------------------------------------------------------------
Version: 0.7.1
Date: soon
Changes:
- Update the detection of special recipes and items, to detect stacking from Deadlock's Beltboxes, caging
from Pyanodon, and pressurization from Pressurized Fluids.
Also detect cases where one direction is required, (e.g. Some Nullius science packs are manufactured in
stacks) and do not consider the required recipe special. (The unstacking recipe, in this case)
As before, special items/recipes are shown at the end of lists and are not selected when ctrl-clicking.
Fixes:
- Display spent fuel items in the production table and link summaries.
- Fix error when switching items in NEIE with middle-click
Expand Down