Beispiel #1
0
 public static FactorioObjectComparer <Recipe> GetRecipeComparerFor(Goods goods)
 {
     return(new FactorioObjectComparer <Recipe>((x, y) => (x.Cost() / x.GetProduction(goods)).CompareTo(y.Cost() / y.GetProduction(goods))));
 }
Beispiel #2
0
 public ProductionTableFlow(Goods goods, float amount, float temperature)
 {
     this.goods       = goods;
     this.amount      = amount;
     this.temperature = temperature;
 }
Beispiel #3
0
        public override void Compute(Project project, ErrorCollector warnings)
        {
            var solver    = DataUtils.CreateSolver("WorkspaceSolver");
            var objective = solver.Objective();

            objective.SetMaximization();
            var time = Stopwatch.StartNew();

            var variables   = Database.goods.CreateMapping <Variable>();
            var constraints = Database.recipes.CreateMapping <Constraint>();

            var sciencePackUsage = new Dictionary <Goods, float>();

            foreach (var technology in Database.technologies.all)
            {
                if (technology.IsAccessible())
                {
                    foreach (var ingredient in technology.ingredients)
                    {
                        if (ingredient.goods.IsAutomatable())
                        {
                            if (onlyCurrentMilestones && !Milestones.Instance.IsAccessibleAtNextMilestone(ingredient.goods))
                            {
                                continue;
                            }
                            sciencePackUsage.TryGetValue(ingredient.goods, out var prev);
                            sciencePackUsage[ingredient.goods] = prev + ingredient.amount * technology.count;
                        }
                    }
                }
            }



            foreach (var goods in Database.goods.all)
            {
                if (!ShouldInclude(goods))
                {
                    continue;
                }
                var mapGeneratedAmount = 0f;
                foreach (var src in goods.miscSources)
                {
                    if (src is Entity ent && ent.mapGenerated)
                    {
                        foreach (var product in ent.loot)
                        {
                            if (product.goods == goods)
                            {
                                mapGeneratedAmount += product.amount;
                            }
                        }
                    }
                }
                var variable = solver.MakeVar(CostLowerLimit, CostLimitWhenGeneratesOnMap / mapGeneratedAmount, false, goods.name);
                objective.SetCoefficient(variable, 1e-3); // adding small amount to each object cost, so even objects that aren't required for science will get cost calculated
                variables[goods] = variable;
            }

            foreach (var(item, count) in sciencePackUsage)
            {
                objective.SetCoefficient(variables[item], count / 1000f);
            }

            var export = Database.objects.CreateMapping <float>();
            var recipeProductionCost = Database.recipesAndTechnologies.CreateMapping <float>();

            recipeCost = Database.recipes.CreateMapping <float>();
            flow       = Database.objects.CreateMapping <float>();
            var lastVariable = Database.goods.CreateMapping <Variable>();

            foreach (var recipe in Database.recipes.all)
            {
                if (!ShouldInclude(recipe))
                {
                    continue;
                }
                if (onlyCurrentMilestones && !recipe.IsAccessibleWithCurrentMilestones())
                {
                    continue;
                }

                // TODO incorporate fuel selection. Now just select fuel if it only uses 1 fuel
                Goods singleUsedFuel       = null;
                var   singleUsedFuelAmount = 0f;
                var   minEmissions         = 100f;
                var   minSize  = 15;
                var   minPower = 1000f;
                foreach (var crafter in recipe.crafters)
                {
                    minEmissions = MathF.Min(crafter.energy.emissions, minEmissions);
                    if (crafter.energy.type == EntityEnergyType.Heat)
                    {
                        break;
                    }
                    if (crafter.size < minSize)
                    {
                        minSize = crafter.size;
                    }
                    var power = crafter.energy.type == EntityEnergyType.Void ? 0f : recipe.time * crafter.power / (crafter.craftingSpeed * crafter.energy.effectivity);
                    if (power < minPower)
                    {
                        minPower = power;
                    }
                    foreach (var fuel in crafter.energy.fuels)
                    {
                        if (!ShouldInclude(fuel))
                        {
                            continue;
                        }
                        if (fuel.fuelValue <= 0f)
                        {
                            singleUsedFuel = null;
                            break;
                        }
                        var amount = power / fuel.fuelValue;
                        if (singleUsedFuel == null)
                        {
                            singleUsedFuel       = fuel;
                            singleUsedFuelAmount = amount;
                        }
                        else if (singleUsedFuel == fuel)
                        {
                            singleUsedFuelAmount = MathF.Min(singleUsedFuelAmount, amount);
                        }
                        else
                        {
                            singleUsedFuel = null;
                            break;
                        }
                    }
                    if (singleUsedFuel == null)
                    {
                        break;
                    }
                }

                if (minPower < 0f)
                {
                    minPower = 0f;
                }
                var size          = Math.Max(minSize, (recipe.ingredients.Length + recipe.products.Length) / 2);
                var sizeUsage     = CostPerSecond * recipe.time * size;
                var logisticsCost = sizeUsage * (1f + CostPerIngredientPerSize * recipe.ingredients.Length + CostPerProductPerSize * recipe.products.Length) + CostPerMj * minPower;

                if (singleUsedFuel == Database.electricity || singleUsedFuel == Database.voidEnergy || singleUsedFuel == Database.heat)
                {
                    singleUsedFuel = null;
                }

                var constraint = solver.MakeConstraint(double.NegativeInfinity, 0, recipe.name);
                constraints[recipe] = constraint;

                foreach (var product in recipe.products)
                {
                    var var    = variables[product.goods];
                    var amount = product.amount;
                    constraint.SetCoefficientCheck(var, amount, ref lastVariable[product.goods]);
                    if (product.goods is Item)
                    {
                        logisticsCost += amount * CostPerItem;
                    }
                    else if (product.goods is Fluid)
                    {
                        logisticsCost += amount * CostPerFluid;
                    }
                }

                if (singleUsedFuel != null)
                {
                    var var = variables[singleUsedFuel];
                    constraint.SetCoefficientCheck(var, -singleUsedFuelAmount, ref lastVariable[singleUsedFuel]);
                }

                foreach (var ingredient in recipe.ingredients)
                {
                    var var = variables[ingredient.goods]; // TODO split cost analysis
                    constraint.SetCoefficientCheck(var, -ingredient.amount, ref lastVariable[ingredient.goods]);
                    if (ingredient.goods is Item)
                    {
                        logisticsCost += ingredient.amount * CostPerItem;
                    }
                    else if (ingredient.goods is Fluid)
                    {
                        logisticsCost += ingredient.amount * CostPerFluid;
                    }
                }

                if (recipe.sourceEntity != null && recipe.sourceEntity.mapGenerated)
                {
                    var totalMining = 0f;
                    foreach (var product in recipe.products)
                    {
                        totalMining += product.amount;
                    }
                    var miningPenalty = MiningPenalty;
                    var totalDensity  = recipe.sourceEntity.mapGenDensity / totalMining;
                    if (totalDensity < MiningMaxDensityForPenalty)
                    {
                        var extraPenalty = MathF.Log(MiningMaxDensityForPenalty / totalDensity);
                        miningPenalty += Math.Min(extraPenalty, MiningMaxExtraPenaltyForRarity);
                    }

                    logisticsCost *= miningPenalty;
                }

                if (minEmissions >= 0f)
                {
                    logisticsCost += minEmissions * CostPerPollution * recipe.time;
                }

                constraint.SetUb(logisticsCost);
                export[recipe]     = logisticsCost;
                recipeCost[recipe] = logisticsCost;
            }

            // TODO this is temporary fix for strange item sources (make the cost of item not higher than the cost of its source)
            foreach (var item in Database.items.all)
            {
                if (ShouldInclude(item))
                {
                    foreach (var source in item.miscSources)
                    {
                        if (source is Goods g && ShouldInclude(g))
                        {
                            var constraint = solver.MakeConstraint(double.NegativeInfinity, 0, "source-" + item.locName);
                            constraint.SetCoefficient(variables[g], -1);
                            constraint.SetCoefficient(variables[item], 1);
                        }
                    }
                }
            }

            // TODO this is temporary fix for fluid temperatures (make the cost of fluid with lower temp not higher than the cost of fluid with higher temp)
            foreach (var(name, fluids) in Database.fluidVariants)
            {
                var prev = fluids[0];
                for (var i = 1; i < fluids.Count; i++)
                {
                    var cur        = fluids[i];
                    var constraint = solver.MakeConstraint(double.NegativeInfinity, 0, "fluid-" + name + "-" + prev.temperature);
                    constraint.SetCoefficient(variables[prev], 1);
                    constraint.SetCoefficient(variables[cur], -1);
                    prev = cur;
                }
            }

            var result = solver.TrySolvewithDifferentSeeds();

            Console.WriteLine("Cost analysis completed in " + time.ElapsedMilliseconds + " ms. with result " + result);
            var sumImportance = 1f;
            var totalRecipes  = 0;

            if (result == Solver.ResultStatus.OPTIMAL || result == Solver.ResultStatus.FEASIBLE)
            {
                var objectiveValue = (float)objective.Value();
                Console.WriteLine("Estimated modpack cost: " + DataUtils.FormatAmount(objectiveValue * 1000f, UnitOfMeasure.None));
                foreach (var g in Database.goods.all)
                {
                    if (variables[g] == null)
                    {
                        continue;
                    }
                    var value = (float)variables[g].SolutionValue();
                    export[g] = value;
                }

                foreach (var recipe in Database.recipes.all)
                {
                    if (constraints[recipe] == null)
                    {
                        continue;
                    }
                    var recipeFlow = (float)constraints[recipe].DualValue();
                    if (recipeFlow > 0f)
                    {
                        totalRecipes++;
                        sumImportance += recipeFlow;
                        flow[recipe]   = recipeFlow;
                        foreach (var product in recipe.products)
                        {
                            flow[product.goods] += recipeFlow * product.amount;
                        }
                    }
                }
            }
            foreach (var o in Database.objects.all)
            {
                if (!ShouldInclude(o))
                {
                    export[o] = float.PositiveInfinity;
                    continue;
                }

                if (o is RecipeOrTechnology recipe)
                {
                    foreach (var ingredient in recipe.ingredients) // TODO split
                    {
                        export[o] += export[ingredient.goods] * ingredient.amount;
                    }
                    foreach (var product in recipe.products)
                    {
                        recipeProductionCost[recipe] += product.amount * export[product.goods];
                    }
                }
                else if (o is Entity entity)
                {
                    var minimal = float.PositiveInfinity;
                    foreach (var item in entity.itemsToPlace)
                    {
                        if (export[item] < minimal)
                        {
                            minimal = export[item];
                        }
                    }
                    export[o] = minimal;
                }
            }
            cost = export;
            recipeProductCost = recipeProductionCost;

            recipeWastePercentage = Database.recipes.CreateMapping <float>();
            if (result == Solver.ResultStatus.OPTIMAL || result == Solver.ResultStatus.FEASIBLE)
            {
                foreach (var(recipe, constraint) in constraints)
                {
                    if (constraint == null)
                    {
                        continue;
                    }
                    var productCost = 0f;
                    foreach (var product in recipe.products)
                    {
                        productCost += product.amount * export[product.goods];
                    }
                    recipeWastePercentage[recipe] = 1f - productCost / export[recipe];
                }
            }
            else
            {
                if (!onlyCurrentMilestones)
                {
                    warnings.Error("Cost analysis was unable to process this modpack. This may mean YAFC bug.", ErrorSeverity.AnalysisWarning);
                }
            }

            importantItems = Database.goods.all.Where(x => x.usages.Length > 1).OrderByDescending(x => flow[x] * cost[x] * x.usages.Count(y => ShouldInclude(y) && recipeWastePercentage[y] == 0f)).ToArray();

            solver.Dispose();
        }
Beispiel #4
0
        public bool FillModules(RecipeParameters recipeParams, Recipe recipe, Entity entity, Goods fuel, Item forceModule, out ModuleEffects effects, out RecipeParameters.UsedModule used)
        {
            effects = new ModuleEffects();
            var isMining   = recipe.flags.HasFlags(RecipeFlags.UsesMiningProductivity);
            var hasEffects = false;

            if (isMining && miningProductivity > 0f)
            {
                effects.productivity += 0.01f * miningProductivity;
                hasEffects            = true;
            }
            used = default;
            if (!isMining && beacon != null && beaconModule != null)
            {
                effects.AddModules(beaconModule.module, beaconsPerBuilding * beacon.beaconEfficiency * beacon.moduleSlots, entity.allowedEffects);
                used.beacon      = beacon;
                used.beaconCount = beaconsPerBuilding;
                hasEffects       = true;
            }
            if (forceModule != null)
            {
                AddModuleSimple(forceModule, ref effects, entity, ref used);
                return(true);
            }
            if (fillMiners || !isMining)
            {
                var  productivityEconomy = recipe.Cost() / recipeParams.recipeTime;
                var  effectivityEconomy  = recipeParams.fuelUsagePerSecondPerBuilding * fuel?.Cost() ?? 0;
                var  bestEconomy         = 0f;
                Item usedModule          = null;
                foreach (var module in recipe.modules)
                {
                    if (module.IsAccessibleWithCurrentMilestones() && entity.CanAcceptModule(module.module))
                    {
                        var economy = MathF.Max(0f, module.module.productivity) * productivityEconomy - module.module.consumption * effectivityEconomy;
                        if (economy > bestEconomy && module.Cost() / economy <= autoFillPayback)
                        {
                            bestEconomy = economy;
                            usedModule  = module;
                        }
                    }
                }

                if (usedModule != null)
                {
                    effects.AddModules(usedModule.module, entity.moduleSlots);
                    used.module = usedModule;
                    used.count  = entity.moduleSlots;
                    return(true);
                }
            }

            if (fillerModule?.module != null && entity.CanAcceptModule(fillerModule.module))
            {
                AddModuleSimple(fillerModule, ref effects, entity, ref used);
                hasEffects = true;
            }
            return(hasEffects);
        }
Beispiel #5
0
 public ProductionTableFlow(Goods goods, float amount, ProductionLink link)
 {
     this.goods  = goods;
     this.amount = amount;
     this.link   = link;
 }