public override void Compute(Project project, ErrorCollector warnings) { var time = Stopwatch.StartNew(); var state = Database.objects.CreateMapping <ProcessingState>(); state[Database.voidEnergy] = ProcessingState.Automatable; var processingQueue = new Queue <FactorioId>(Database.objects.count); var unknowns = 0; foreach (var recipe in Database.recipes.all) { var hasAutomatableCrafter = false; foreach (var crafter in recipe.crafters) { if (crafter != Database.character && crafter.IsAccessible()) { hasAutomatableCrafter = true; } } if (!hasAutomatableCrafter) { state[recipe] = ProcessingState.NotAutomatable; } } foreach (var obj in Database.objects.all) { if (!obj.IsAccessible()) { state[obj] = ProcessingState.NotAutomatable; } else if (state[obj] == ProcessingState.Unknown) { unknowns++; state[obj] = ProcessingState.UnknownInQueue; processingQueue.Enqueue(obj.id); } } while (processingQueue.Count > 0) { var index = processingQueue.Dequeue(); var dependencies = Dependencies.dependencyList[index]; var automationState = ProcessingState.Automatable; foreach (var depGroup in dependencies) { if (depGroup.flags.HasFlags(DependencyList.Flags.OneTimeInvestment)) { continue; } if (depGroup.flags.HasFlags(DependencyList.Flags.RequireEverything)) { foreach (var element in depGroup.elements) { if (state[element] < automationState) { automationState = state[element]; } } } else { var localHighest = ProcessingState.NotAutomatable; foreach (var element in depGroup.elements) { if (state[element] > localHighest) { localHighest = state[element]; } } if (localHighest < automationState) { automationState = localHighest; } } } if (automationState == ProcessingState.UnknownInQueue) { automationState = ProcessingState.Unknown; } state[index] = automationState; if (automationState != ProcessingState.Unknown) { unknowns--; foreach (var revDep in Dependencies.reverseDependencies[index]) { if (state[revDep] == ProcessingState.Unknown) { processingQueue.Enqueue(revDep); state[revDep] = ProcessingState.UnknownInQueue; } } } } state[Database.voidEnergy] = ProcessingState.NotAutomatable; Console.WriteLine("Automation analysis (first pass) finished in " + time.ElapsedMilliseconds + " ms. Unknowns left: " + unknowns); if (unknowns > 0) { // TODO run graph analysis if there are any unknowns left... Right now assume they are not automatable } automatable = state.Remap((_, s) => s == ProcessingState.Automatable); }
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(); }
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()) { sciencePackUsage.TryGetValue(ingredient.goods, out var prev); sciencePackUsage[ingredient.goods] = prev + ingredient.amount * technology.count; } } } } foreach (var goods in Database.goods.all) { if (!goods.IsAutomatable()) { 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); var baseItemCost = (goods.usages.Length + 1) * 0.01f; if (goods is Item item && (item.factorioType != "item" || item.placeResult != null)) { baseItemCost += 0.1f; } if (goods.fuelValue > 0f) { baseItemCost += goods.fuelValue * 0.0001f; } objective.SetCoefficient(variable, baseItemCost); 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 (!recipe.IsAutomatable()) { continue; } var logisticsCost = (CostPerIngredient * recipe.ingredients.Length + CostPerProduct * recipe.products.Length + CostPerSecond) * recipe.time; // TODO incorporate fuel selection. Now just select fuel if it only uses 1 fuel Goods singleUsedFuel = null; var singleUsedFuelAmount = 0f; var minEmissions = 100f; foreach (var crafter in recipe.crafters) { minEmissions = MathF.Min(crafter.energy.emissions, minEmissions); if (crafter.energy.usesHeat) { break; } foreach (var fuel in crafter.energy.fuels) { if (!fuel.IsAutomatable()) { continue; } var amount = (recipe.time * crafter.power) / (crafter.energy.effectivity * 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 (singleUsedFuel == Database.electricity || singleUsedFuel == Database.voidEnergy) { 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]; 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; } else { logisticsCost = MathF.Max(logisticsCost * 0.5f, logisticsCost + minEmissions * CostPerPollution * recipe.time); // only allow cut logistics cost by half with negative emissions } constraint.SetUb(logisticsCost); export[recipe] = logisticsCost; recipeCost[recipe] = logisticsCost; } // TODO this is temporary fix for strange item sources foreach (var goods in Database.goods.all) { if (goods is Item item && item.IsAutomatable()) { foreach (var source in item.miscSources) { if (source is Goods g && g.IsAutomatable()) { var constraint = solver.MakeConstraint(double.NegativeInfinity, 0, "source-" + item.locName); constraint.SetCoefficient(variables[g], -1); constraint.SetCoefficient(variables[item], 1); } } } } 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 (!g.IsAutomatable()) { continue; } var value = (float)variables[g].SolutionValue(); export[g] = value; } foreach (var recipe in Database.recipes.all) { if (!recipe.IsAutomatable()) { 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; } } } flowRecipeScaleCoef = (1e2f * totalRecipes) / (sumImportance * MathF.Sqrt(MathF.Sqrt(objectiveValue))); } foreach (var o in Database.objects.all) { if (!o.IsAutomatable()) { export[o] = float.PositiveInfinity; continue; } if (o is RecipeOrTechnology recipe) { foreach (var ingredient in recipe.ingredients) { 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 * product.goods.Cost(); } recipeWastePercentage[recipe] = 1f - productCost / cost[recipe]; } } else { 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 => y.IsAutomatable() && recipeWastePercentage[y] == 0f)).ToArray(); solver.Dispose(); }
public void ComputeWithParameters(Project project, ErrorCollector warnings, FactorioObject[] milestones, bool autoSort) { if (this.project == null) { this.project = project; project.settings.changed += ProjectSettingsChanged; } var time = Stopwatch.StartNew(); var result = Database.objects.CreateMapping <ulong>(); var processing = Database.objects.CreateMapping <ProcessingFlags>(); var processingQueue = new Queue <FactorioId>(); foreach (var rootAccessbile in Database.rootAccessible) { result[rootAccessbile] = 1; processingQueue.Enqueue(rootAccessbile.id); processing[rootAccessbile] = ProcessingFlags.Initial | ProcessingFlags.InQueue; } foreach (var(obj, flag) in project.settings.itemFlags) { if (flag.HasFlags(ProjectPerItemFlags.MarkedAccessible)) { result[obj] = 1; processingQueue.Enqueue(obj.id); processing[obj] = ProcessingFlags.Initial | ProcessingFlags.InQueue; } else if (flag.HasFlag(ProjectPerItemFlags.MarkedInaccessible)) { processing[obj] = ProcessingFlags.ForceInaccessible; } } if (autoSort) { // Adding default milestones AND special flag to auto-order them foreach (var milestone in milestones) { processing[milestone] |= ProcessingFlags.MilestoneNeedOrdering; } currentMilestones = new FactorioObject[milestones.Length]; } else { currentMilestones = milestones; for (var i = 0; i < milestones.Length; i++) { result[milestones[i]] = (1ul << (i + 1)) | 1; } } var dependencyList = Dependencies.dependencyList; var reverseDependencies = Dependencies.reverseDependencies; List <FactorioObject> milestonesNotReachable = null; var nextMilestoneMask = 0x2ul; var nextMilestoneIndex = 0; var accessibleObjects = 0; var flagMask = 0ul; for (var i = 0; i <= currentMilestones.Length; i++) { flagMask |= 1ul << i; if (i > 0) { var milestone = currentMilestones[i - 1]; if (milestone == null) { milestonesNotReachable = new List <FactorioObject>(); foreach (var pack in Database.allSciencePacks) { if (Array.IndexOf(currentMilestones, pack) == -1) { currentMilestones[nextMilestoneIndex++] = pack; milestonesNotReachable.Add(pack); } } Array.Resize(ref currentMilestones, nextMilestoneIndex); break; } Console.WriteLine("Processing milestone " + milestone.locName); processingQueue.Enqueue(milestone.id); processing[milestone] = ProcessingFlags.Initial | ProcessingFlags.InQueue; } while (processingQueue.Count > 0) { var elem = processingQueue.Dequeue(); var entry = dependencyList[elem]; var cur = result[elem]; var eflags = cur; var isInitial = (processing[elem] & ProcessingFlags.Initial) != 0; processing[elem] &= ProcessingFlags.MilestoneNeedOrdering; foreach (var list in entry) { if ((list.flags & DependencyList.Flags.RequireEverything) != 0) { foreach (var req in list.elements) { var reqFlags = result[req]; if (reqFlags == 0 && !isInitial) { goto skip; } eflags |= result[req]; } } else { var groupFlags = 0ul; foreach (var req in list.elements) { var acc = result[req]; if (acc == 0) { continue; } if (acc < groupFlags || groupFlags == 0ul) { groupFlags = acc; } } if (groupFlags == 0 && !isInitial) { goto skip; } eflags |= groupFlags; } } if (!isInitial) { if (eflags == cur || (eflags | flagMask) != flagMask) { continue; } } else { eflags &= flagMask; } accessibleObjects++; //var obj = Database.objects[elem]; //Console.WriteLine("Added object "+obj.locName+" ["+obj.GetType().Name+"] with mask "+eflags.ToString("X") + " (was "+cur.ToString("X")+")"); if (processing[elem] == ProcessingFlags.MilestoneNeedOrdering) { processing[elem] = 0; eflags |= nextMilestoneMask; nextMilestoneMask <<= 1; currentMilestones[nextMilestoneIndex++] = Database.objects[elem]; } result[elem] = eflags; foreach (var revdep in reverseDependencies[elem]) { if ((processing[revdep] & ~ProcessingFlags.MilestoneNeedOrdering) != 0 || result[revdep] != 0) { continue; } processing[revdep] |= ProcessingFlags.InQueue; processingQueue.Enqueue(revdep); } skip :; } } if (!project.settings.milestones.SequenceEqual(currentMilestones)) { project.settings.RecordUndo(); project.settings.milestones.Clear(); project.settings.milestones.AddRange(currentMilestones); } GetLockedMaskFromProject(); var hasAutomatableRocketLaunch = result[Database.objectsByTypeName["Special.launch"]] != 0; if (accessibleObjects < Database.objects.count / 2) { warnings.Error("More than 50% of all in-game objects appear to be inaccessible in this project with your current mod list. This can have a variety of reasons like objects being accessible via scripts," + MaybeBug + MilestoneAnalysisIsImportant + UseDependencyExplorer, ErrorSeverity.AnalysisWarning); } else if (!hasAutomatableRocketLaunch) { warnings.Error("Rocket launch appear to be inaccessible. This means that rocket may not be launched in this mod pack, or it requires mod script to spawn or unlock some items," + MaybeBug + MilestoneAnalysisIsImportant + UseDependencyExplorer, ErrorSeverity.AnalysisWarning); } else if (milestonesNotReachable != null) { warnings.Error("There are some milestones that are not accessible: " + string.Join(", ", milestonesNotReachable.Select(x => x.locName)) + ". You may remove these from milestone list," + MaybeBug + MilestoneAnalysisIsImportant + UseDependencyExplorer, ErrorSeverity.AnalysisWarning); } Console.WriteLine("Milestones calculation finished in " + time.ElapsedMilliseconds + " ms."); milestoneResult = result; }