diff --git a/Yafc.Model/Analysis/Dependencies.cs b/Yafc.Model/Analysis/Dependencies.cs index 47a73998..81f7d9f9 100644 --- a/Yafc.Model/Analysis/Dependencies.cs +++ b/Yafc.Model/Analysis/Dependencies.cs @@ -1,34 +1,7 @@ -using System; -using System.Collections.Generic; -using System.Linq; +using System.Collections.Generic; namespace Yafc.Model; -public struct DependencyList(FactorioId[] elements, DependencyList.Flags flags) { - [Flags] - public enum Flags { - RequireEverything = 0x100, - OneTimeInvestment = 0x200, - - Ingredient = 1 | RequireEverything, - CraftingEntity = 2 | OneTimeInvestment, - SourceEntity = 3 | OneTimeInvestment, - TechnologyUnlock = 4 | OneTimeInvestment, - Source = 5, - Fuel = 6, - ItemToPlace = 7, - TechnologyPrerequisites = 8 | RequireEverything | OneTimeInvestment, - IngredientVariant = 9, - Hidden = 10, - Location = 11 | OneTimeInvestment, - } - - public Flags flags = flags; - public FactorioId[] elements = elements; - - public DependencyList(IEnumerable elements, Flags flags) : this(elements.Select(o => o.id).ToArray(), flags) { } -} - public static class Dependencies { /// /// The objects the key requires, organized into useful categories. Some categories are requires-any, others are requires-all. diff --git a/Yafc.Model/Analysis/DependencyNode.cs b/Yafc.Model/Analysis/DependencyNode.cs index 20e2e637..2a63c1c8 100644 --- a/Yafc.Model/Analysis/DependencyNode.cs +++ b/Yafc.Model/Analysis/DependencyNode.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Linq; using System.Numerics; using Yafc.UI; @@ -14,43 +15,51 @@ namespace Yafc.Model; /// This represents one node in a dependency tree, which may be the root node of the tree or the child of another node. /// public abstract class DependencyNode { + [Flags] + public enum Flags { + RequireEverything = 0x100, + OneTimeInvestment = 0x200, + + Ingredient = 1 | RequireEverything, + CraftingEntity = 2 | OneTimeInvestment, + SourceEntity = 3 | OneTimeInvestment, + TechnologyUnlock = 4 | OneTimeInvestment, + Source = 5, + Fuel = 6, + ItemToPlace = 7, + TechnologyPrerequisites = 8 | RequireEverything | OneTimeInvestment, + IngredientVariant = 9, + Hidden = 10, + Location = 11 | OneTimeInvestment, + } + private DependencyNode() { } // All derived classes should be nested classes /// - /// Creates a from a . contains the require-any/-all + /// Creates a from a list of dependencies. contains the require-any/-all /// behavior and information about how the dependencies should be described. (e.g. "Crafter", "Ingredient", etc.) /// - public static DependencyNode Create(DependencyList dependencies) => new ListNode(dependencies); + public static DependencyNode Create(IEnumerable elements, Flags flags) => new ListNode(elements, flags); /// /// Creates a that is satisfied if all of its child nodes are satisfied. - /// This matches the old behavior for []. - /// - public static DependencyNode RequireAll(IEnumerable dependencies) => AndNode.Create(dependencies.Select(Create)); - /// - /// Creates a that is satisfied if all of its child nodes are satisfied. - /// This matches the old behavior for []. + /// This matches the old behavior for a legacy DependencyList[]. /// public static DependencyNode RequireAll(IEnumerable dependencies) => AndNode.Create(dependencies); /// /// Creates a that is satisfied if all of its child nodes are satisfied. - /// This matches the old behavior for []. + /// This matches the old behavior for a legacy DependencyList[]. /// public static DependencyNode RequireAll(params DependencyNode[] dependencies) => AndNode.Create(dependencies); /// /// Creates a that is satisfied if any of its child nodes are satisfied. This behavior was only accessible - /// within a single , by not setting . - /// - public static DependencyNode RequireAny(IEnumerable dependencies) => OrNode.Create(dependencies.Select(Create)); - /// - /// Creates a that is satisfied if any of its child nodes are satisfied. This behavior was only accessible - /// within a single , by not setting . + /// within a single legacy DependencyList, by not setting . /// public static DependencyNode RequireAny(params DependencyNode[] dependencies) => OrNode.Create(dependencies); /// /// Creates a that is satisfied if any of its child nodes are satisfied. This behavior was only accessible - /// within a single , by not setting . + /// within a single legacy DependencyList, by not setting . /// public static DependencyNode RequireAny(IEnumerable dependencies) => OrNode.Create(dependencies); @@ -92,8 +101,8 @@ private DependencyNode() { } // All derived classes should be nested classes /// Instructs this dependency tree to draw itself on the specified . /// /// The drawing destination. - /// A delegate that will draw the passed onto the passed . - public abstract void Draw(ImGui gui, Action builder); + /// A delegate that will draw the passed dependency information onto the passed . + public abstract void Draw(ImGui gui, Action, Flags> builder); /// /// A that requires all of its children. @@ -132,7 +141,17 @@ internal static DependencyNode Create(IEnumerable dependencies) internal override IEnumerable Flatten() => dependencies.SelectMany(d => d.Flatten()); - internal override bool IsAccessible(Func isAccessible) => dependencies.All(d => d.IsAccessible(isAccessible)); + internal override bool IsAccessible(Func isAccessible) { + // Use foreach instead of dependencies.All(d => d.IsAccessible(isAccessible)) to reduce allocations and increase speed. + // Unlike ListNode, switching to for here did not significantly improve speed. + foreach (DependencyNode item in dependencies) { + if (!item.IsAccessible(isAccessible)) { + return false; + } + } + return true; + } + internal override Bits AggregateBits(Func getBits) { Bits result = default; foreach (DependencyNode item in dependencies) { @@ -143,7 +162,7 @@ internal override Bits AggregateBits(Func getBits) { internal override AutomationStatus IsAutomatable(Func isAutomatable, AutomationStatus automationState) => dependencies.Min(d => d.IsAutomatable(isAutomatable, automationState)); - public override void Draw(ImGui gui, Action builder) { + public override void Draw(ImGui gui, Action, Flags> builder) { bool previousChildWasOr = false; foreach (DependencyNode dependency in dependencies) { if (dependency is OrNode && previousChildWasOr) { @@ -198,7 +217,7 @@ internal static DependencyNode Create(IEnumerable dependencies) internal override AutomationStatus IsAutomatable(Func isAutomatable, AutomationStatus automationState) => dependencies.Max(d => d.IsAutomatable(isAutomatable, automationState)); - public override void Draw(ImGui gui, Action builder) { + public override void Draw(ImGui gui, Action, Flags> builder) { Vector2 offset = new(.4f, 0); using (gui.EnterGroup(new(1f, 0, 0, 0))) { bool isFirst = true; @@ -221,37 +240,49 @@ public override void Draw(ImGui gui, Action builder) { /// A that matches the behavior of a legacy . /// /// The whose behavior should be matched by this . - private sealed class ListNode(DependencyList dependencies) : DependencyNode { - private readonly DependencyList dependencies = dependencies; + private sealed class ListNode(IEnumerable elements, Flags flags) : DependencyNode { + private readonly ReadOnlyCollection elements = elements.Select(e => e.id).ToList().AsReadOnly(); - internal override IEnumerable Flatten() => dependencies.elements; + internal override IEnumerable Flatten() => elements; internal override bool IsAccessible(Func isAccessible) { - if (dependencies.flags.HasFlag(DependencyList.Flags.RequireEverything)) { - return dependencies.elements.All(isAccessible); + // Use for instead of foreach or elements.All(isAccessible) to reduce allocations and increase speed. + if (flags.HasFlags(Flags.RequireEverything)) { + for (int i = 0; i < elements.Count; i++) { + if (!isAccessible(elements[i])) { + return false; + } + } + return true; + } + + for (int i = 0; i < elements.Count; i++) { + if (isAccessible(elements[i])) { + return true; + } } - return dependencies.elements.Any(isAccessible); + return false; } internal override Bits AggregateBits(Func getBits) { Bits bits = new(); - if (dependencies.flags.HasFlag(DependencyList.Flags.RequireEverything)) { - foreach (FactorioId item in dependencies.elements) { + if (flags.HasFlags(Flags.RequireEverything)) { + foreach (FactorioId item in elements) { bits |= getBits(item); } return bits; } - else if (dependencies.elements.Length > 0) { - return bits | dependencies.elements.Min(getBits); + else if (elements.Count > 0) { + return bits | elements.Min(getBits); } return bits; } internal override AutomationStatus IsAutomatable(Func getAutomation, AutomationStatus automationState) { // Copied from AutomationAnalysis.cs. - if (!dependencies.flags.HasFlags(DependencyList.Flags.OneTimeInvestment)) { - if (dependencies.flags.HasFlags(DependencyList.Flags.RequireEverything)) { - foreach (FactorioId element in dependencies.elements) { + if (!flags.HasFlags(Flags.OneTimeInvestment)) { + if (flags.HasFlags(Flags.RequireEverything)) { + foreach (FactorioId element in elements) { if (getAutomation(element) < automationState) { automationState = getAutomation(element); } @@ -260,7 +291,7 @@ internal override AutomationStatus IsAutomatable(Func localHighest) { localHighest = getAutomation(element); } @@ -271,11 +302,11 @@ internal override AutomationStatus IsAutomatable(Func builder) => builder(gui, dependencies); + public override void Draw(ImGui gui, Action, Flags> builder) => builder(gui, elements, flags); } - public static implicit operator DependencyNode(DependencyList list) => Create(list); + public static implicit operator DependencyNode((IEnumerable elements, Flags flags) value) + => Create(value.elements, value.flags); } diff --git a/Yafc.Model/Analysis/Milestones.cs b/Yafc.Model/Analysis/Milestones.cs index 474d4601..aa7dc02c 100644 --- a/Yafc.Model/Analysis/Milestones.cs +++ b/Yafc.Model/Analysis/Milestones.cs @@ -1,7 +1,9 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using System.Threading.Tasks; using Serilog; using Yafc.UI; @@ -110,7 +112,7 @@ public void ComputeWithParameters(Project project, ErrorCollector warnings, Fact } Stopwatch time = Stopwatch.StartNew(); - Dictionary> accessibility = []; + ConcurrentDictionary accessibility = []; const FactorioId noObject = (FactorioId)(-1); List sortedMilestones = []; @@ -121,47 +123,17 @@ public void ComputeWithParameters(Project project, ErrorCollector warnings, Fact } } - foreach (FactorioObject? milestone in milestones.Prepend(null)) { - logger.Information("Processing milestone {Milestone}", milestone?.locName); - // Queue the known-accessible items for graph walking, and mark them accessible without the current milestone. - Queue processingQueue = new(Database.rootAccessible); - foreach ((FactorioObject obj, ProjectPerItemFlags flag) in project.settings.itemFlags) { - if (flag.HasFlags(ProjectPerItemFlags.MarkedAccessible)) { - processingQueue.Enqueue(obj); - } - } - HashSet accessibleWithoutMilestone = accessibility[milestone?.id ?? noObject] = new(processingQueue); - - // Walk the dependency graph to find accessible items. The first walk, when milestone == null, is for basic accessibility. - // The rest of the walks prune the graph at the selected milestone and are for milestone flags. - while (processingQueue.TryDequeue(out FactorioObject? node)) { - if (node == milestone || markedInaccessible.Contains(node)) { - // We're looking for things that can be accessed without this milestone, or the user flagged this as inaccessible. - continue; - } - - bool accessible = true; - if (!accessibleWithoutMilestone.Contains(node)) { - // This object is accessible if all its parents are accessible. - accessible = Dependencies.dependencyList[node].IsAccessible(e => accessibleWithoutMilestone.Contains(Database.objects[e])); - } + // Walk the accessibility graph to find accessible items. The first walk is for basic accessibility and milestone sorting. + logger.Information("Processing object accessibility"); + accessibility[noObject] = WalkAccessibilityGraph(project, markedInaccessible, milestones, sortedMilestones); - if (accessible) { - accessibleWithoutMilestone.Add(node); - if (milestones.Contains(node) && !sortedMilestones.Contains(node)) { - // Sort milestones in the order we unlock them in the accessibility walk. - sortedMilestones.Add(node); - } - - // Recheck this objects children, if necessary. - foreach (FactorioObject child in Dependencies.reverseDependencies[node].Select(id => Database.objects[id])) { - if (!accessibleWithoutMilestone.Contains(child) && !processingQueue.Contains(child)) { - processingQueue.Enqueue(child); - } - } - } - } - } + // The rest of the walks prune the graph at the selected milestone, for computing milestone flags. + // Do these in parallel; the only write operation for these is setting the dictionary value at the end. + Parallel.ForEach(milestones, milestone => { + logger.Information("Processing milestone {Milestone}", milestone.locName); + HashSet pruneAt = new(markedInaccessible.Append(milestone)); + accessibility[milestone.id] = WalkAccessibilityGraph(project, pruneAt, [], null); + }); // Apply the milestone sort results, if requested. if (autoSort) { @@ -174,12 +146,12 @@ public void ComputeWithParameters(Project project, ErrorCollector warnings, Fact // Turn the walk results into milestone bitmasks. Mapping result = Database.objects.CreateMapping(); - foreach (FactorioObject obj in accessibility[noObject]) { + foreach (FactorioObject obj in Database.objects.all.Where(o => accessibility[noObject][(int)o.id])) { Bits bits = new(true); for (int i = 0; i < currentMilestones.Length; i++) { FactorioObject milestone = currentMilestones[i]; - if (!accessibility[milestone.id].Contains(obj)) { + if (!accessibility[milestone.id][(int)obj.id]) { bits[i + 1] = true; } } @@ -187,7 +159,7 @@ public void ComputeWithParameters(Project project, ErrorCollector warnings, Fact } // Predict the milestone mask for inaccessible items by OR-ing the masks for their parents. - Queue inaccessibleQueue = new(Database.objects.all.Except(accessibility[noObject])); + Queue inaccessibleQueue = new(Database.objects.all.Where(o => !accessibility[noObject][(int)o.id])); while (inaccessibleQueue.TryDequeue(out FactorioObject? inaccessible)) { Bits milestoneBits = Dependencies.dependencyList[inaccessible].AggregateBits(id => result[id]); @@ -212,7 +184,7 @@ public void ComputeWithParameters(Project project, ErrorCollector warnings, Fact } GetLockedMaskFromProject(); - int accessibleObjects = accessibility[noObject].Count; + int accessibleObjects = accessibility[noObject].Count(x => x); bool hasAutomatableRocketLaunch = result[Database.objectsByTypeName["Special.launch"]] != 0; List milestonesNotReachable = [.. milestones.Except(sortedMilestones)]; if (accessibleObjects < Database.objects.count / 2) { @@ -232,6 +204,62 @@ public void ComputeWithParameters(Project project, ErrorCollector warnings, Fact milestoneResult = result; } + /// + /// Walks the accessibility graph, refusing to traverse the specified nodes, and determines what objects are accessible. If requested, also + /// adds objects from to , based on the order they were encountered. + /// + /// The project to be analyzed, for reading . + /// The nodes that should be ignored when walking the graph. + /// The milestones to sort, or an empty array if milestone sorting is not desired. + /// A list that will receive the milestones in the order they were encountered. Must not be + /// if is not empty. + /// + /// An array of bools, where the true values correspond to the s of objects that can be accessed without + /// any of the nodes in . + private static bool[] WalkAccessibilityGraph(Project project, HashSet pruneAt, FactorioObject[] milestones, + List? sortedMilestones) { + + if (milestones.Length != 0) { + ArgumentNullException.ThrowIfNull(sortedMilestones); + } + + // Queue the known-accessible items for graph walking. + Queue accessibleQueue = new(Database.rootAccessible.Except(pruneAt).Select(o => o.id)); + foreach ((FactorioObject obj, ProjectPerItemFlags flag) in project.settings.itemFlags) { + if (flag.HasFlags(ProjectPerItemFlags.MarkedAccessible)) { + accessibleQueue.Enqueue(obj.id); + } + } + bool[] accessibleWithoutPruning = new bool[Database.objects.count]; + foreach (FactorioId item in accessibleQueue) { + accessibleWithoutPruning[(int)item] = true; + } + bool[] prune = new bool[Database.objects.count]; + foreach (FactorioObject item in pruneAt) { + prune[(int)item.id] = true; + } + + while (accessibleQueue.TryDequeue(out FactorioId node)) { + // null-forgiving: sortedMilestones is not null when milestones is not empty. + if (milestones.Contains(Database.objects[node]) && !sortedMilestones!.Contains(Database.objects[node])) { + // Sort milestones in the order we unlock them in the accessibility walk. + sortedMilestones.Add(Database.objects[node]); + } + + // Mark and queue this object's newly-accessible children. + foreach (FactorioId child in Dependencies.reverseDependencies[node]) { + if (!accessibleWithoutPruning[(int)child] && !prune[(int)child] + && Dependencies.dependencyList[child].IsAccessible(x => accessibleWithoutPruning[(int)x])) { + + accessibleWithoutPruning[(int)child] = true; + accessibleQueue.Enqueue(child); + } + } + } + + return accessibleWithoutPruning; + } + private const string MaybeBug = " or it might be due to a bug inside a mod or YAFC."; private const string MilestoneAnalysisIsImportant = "\nA lot of YAFC's systems rely on objects being accessible, so some features may not work as intended."; private const string UseDependencyExplorer = "\n\nFor this reason YAFC has a Dependency Explorer that allows you to manually enable some of the core recipes. " + diff --git a/Yafc.Model/Data/DataClasses.cs b/Yafc.Model/Data/DataClasses.cs index 09635b5b..729962a2 100644 --- a/Yafc.Model/Data/DataClasses.cs +++ b/Yafc.Model/Data/DataClasses.cs @@ -120,28 +120,28 @@ public abstract class RecipeOrTechnology : FactorioObject { public sealed override DependencyNode GetDependencies() => DependencyNode.RequireAll(GetDependenciesHelper()); - protected virtual List GetDependenciesHelper() { - List collector = []; + protected virtual List GetDependenciesHelper() { + List collector = []; if (ingredients.Length > 0) { List ingredients = []; foreach (Ingredient ingredient in this.ingredients) { if (ingredient.variants != null) { - collector.Add(new(ingredient.variants, DependencyList.Flags.IngredientVariant)); + collector.Add((ingredient.variants, DependencyNode.Flags.IngredientVariant)); } else { ingredients.Add(ingredient.goods); } } if (ingredients.Count > 0) { - collector.Add(new(ingredients, DependencyList.Flags.Ingredient)); + collector.Add((ingredients, DependencyNode.Flags.Ingredient)); } } - collector.Add(new(crafters, DependencyList.Flags.CraftingEntity)); + collector.Add((crafters, DependencyNode.Flags.CraftingEntity)); if (sourceEntity != null) { - collector.Add(new([sourceEntity.id], DependencyList.Flags.SourceEntity)); + collector.Add(([sourceEntity], DependencyNode.Flags.SourceEntity)); } if (sourceTiles.Count > 0) { - collector.Add(new([.. sourceTiles.SelectMany(t => t.locations).Distinct()], DependencyList.Flags.Location)); + collector.Add((sourceTiles.SelectMany(t => t.locations).Distinct(), DependencyNode.Flags.Location)); } return collector; @@ -194,13 +194,13 @@ public bool HasIngredientVariants() { return false; } - protected override List GetDependenciesHelper() { - List lists = base.GetDependenciesHelper(); + protected override List GetDependenciesHelper() { + List nodes = base.GetDependenciesHelper(); if (!enabled) { - lists.Add(new(technologyUnlock, DependencyList.Flags.TechnologyUnlock)); + nodes.Add((technologyUnlock, DependencyNode.Flags.TechnologyUnlock)); } - return lists; + return nodes; } public override bool CanAcceptModule(Module module) => EntityWithModules.CanAcceptModule(module.moduleSpecification, allowedEffects, allowedModuleCategories); @@ -353,8 +353,7 @@ public abstract class Goods : FactorioObject { public abstract UnitOfMeasure flowUnitOfMeasure { get; } public bool isLinkable { get; internal set; } = true; - public override DependencyNode GetDependencies() - => DependencyNode.Create(new(production.Concat(miscSources), DependencyList.Flags.Source)); + public override DependencyNode GetDependencies() => (production.Concat(miscSources), DependencyNode.Flags.Source); public virtual bool HasSpentFuel([MaybeNullWhen(false)] out Item spent) { spent = null; @@ -454,8 +453,7 @@ public class Location : FactorioObject { internal override FactorioObjectSortOrder sortingOrder => FactorioObjectSortOrder.Locations; - public override DependencyNode GetDependencies() - => DependencyNode.Create(new(technologyUnlock, DependencyList.Flags.TechnologyUnlock)); + public override DependencyNode GetDependencies() => (technologyUnlock, DependencyNode.Flags.TechnologyUnlock); } public class Special : Goods { @@ -469,7 +467,7 @@ public class Special : Goods { internal override FactorioObjectSortOrder sortingOrder => FactorioObjectSortOrder.SpecialGoods; public override DependencyNode GetDependencies() { if (isResearch) { - return DependencyNode.Create(new(Database.technologies.all, DependencyList.Flags.Source)); + return (Database.technologies.all, DependencyNode.Flags.Source); } else { return base.GetDependencies(); @@ -500,7 +498,7 @@ public class Tile : FactorioObject { internal HashSet locations { get; } = []; - public override DependencyNode GetDependencies() => DependencyNode.Create(new(locations, DependencyList.Flags.Location)); + public override DependencyNode GetDependencies() => (locations, DependencyNode.Flags.Location); } public class Entity : FactorioObject { @@ -540,79 +538,66 @@ public float Power(Quality quality) internal Lazy? getSpoilResult; public sealed override DependencyNode GetDependencies() { - List collector = []; - if (energy != null) { - collector.Add(new DependencyList(energy.fuels, DependencyList.Flags.Fuel)); - } + // All entities require at least one source. Some also require fuel. + // Implemented sources are: + // - map gen location (e.g. spawners, ores, asteroids) + // - itemsToPlace (TODO: requires valid location, possibly not the same as map gen location) + // - asteroid death + // - entity capture (e.g. captured spawners) + // Unimplemented sources include: + // - entity spawn (e.g. biters from spawners) + // - item spoilage (e.g. egg spoilage) + // - most projectile effects (e.g. strafer pentapod projectiles) + // - entity death (e.g. spawner reversion, explosions, corpses) + + List sources = []; + if (mapGenerated) { - if (itemsToPlace.Length != 0) { - collector.Add(DependencyNode.RequireAny( - new DependencyList(spawnLocations, DependencyList.Flags.Location), - new DependencyList(itemsToPlace, DependencyList.Flags.ItemToPlace))); - } - else { - collector.Add(new DependencyList(spawnLocations, DependencyList.Flags.Location)); - } + sources.Add((spawnLocations, DependencyNode.Flags.Location)); } - - if (sourceEntities.Count > 0) { - // Asteroid chunks require locations OR bigger-asteroid - collector.Add(new DependencyList(sourceEntities, DependencyList.Flags.Source)); - return DependencyNode.RequireAny(collector); + if (itemsToPlace.Length > 0) { + sources.Add((itemsToPlace, DependencyNode.Flags.Source)); } - - if (captureAmmo.Count == 0) { - if (!mapGenerated) { - collector.Add(new DependencyList(itemsToPlace, DependencyList.Flags.ItemToPlace)); - } - - return DependencyNode.RequireAll(collector); + if (sourceEntities.Count > 0) { // Asteroid death + sources.Add((sourceEntities, DependencyNode.Flags.Source)); } - // Captive spawners require fuel AND (placement-items OR (spawners AND capture-ammo)) + if (captureAmmo.Count > 0) { + // Capture sources require spawners and capture-ammo - // Find the (ammo, [.. spawner]) pairs that can create this, grouped by ammo. - List<(Ammo ammo, List spawners)> sourceSpawners = []; - foreach (Ammo ammo in captureAmmo) { - List sources; - if (ammo.targetFilter == null) { - sources = Database.objects.all.OfType().Where(s => s.capturedEntityName == name).ToList(); - } - else { - sources = ammo.targetFilter.Select(t => Database.objectsByTypeName["Entity." + t] as EntitySpawner) - .Where(s => s!.capturedEntityName == name).ToList()!; + // Find the (ammo, [.. spawner]) pairs that can create this, grouped by ammo. + List<(Ammo ammo, List spawners)> sourceSpawners = []; + foreach (Ammo ammo in captureAmmo) { + List spawners; + if (ammo.targetFilter == null) { + spawners = Database.objects.all.OfType().Where(s => s.capturedEntityName == name).ToList(); + } + else { + spawners = ammo.targetFilter.Select(t => Database.objectsByTypeName["Entity." + t] as EntitySpawner) + .Where(s => s?.capturedEntityName == name).ToList()!; + } + sourceSpawners.Add((ammo, spawners)); } - sourceSpawners.Add((ammo, sources)); - } - // group the ammo by spawner list, to make ([.. ammo], [.. spawner]) pairs. - var groups = sourceSpawners.GroupBy(s => s.spawners, new ListComparer()).Select(g => (g.Select(l => l.ammo).ToList(), g.Key)).ToList(); + // group the ammo by spawner list, to make ([.. ammo], [.. spawner]) pairs. + var groups = sourceSpawners.GroupBy(s => s.spawners, new ListComparer()).Select(g => (g.Select(l => l.ammo), g.Key)); - List ammoPlusSpawner = []; - foreach ((List ammo, List spawners) in groups) { - ammoPlusSpawner.Add(DependencyNode.RequireAll( - DependencyNode.Create(new(ammo, DependencyList.Flags.Source)), - DependencyNode.Create(new(spawners, DependencyList.Flags.Source)) - )); + foreach ((IEnumerable ammo, List spawners) in groups) { + sources.Add(DependencyNode.RequireAll((ammo, DependencyNode.Flags.Source), (spawners, DependencyNode.Flags.Source))); + } } - // The non-fuel requirements - List nonFuel = []; - if (itemsToPlace.Length > 0) { - nonFuel.Add(DependencyNode.Create(new(itemsToPlace, DependencyList.Flags.ItemToPlace))); - } - if (ammoPlusSpawner.Count > 0) { - nonFuel.Add(DependencyNode.RequireAny(ammoPlusSpawner)); - } - if (nonFuel.Count == 0) { - nonFuel.Add(DependencyNode.Create(new(Array.Empty(), DependencyList.Flags.Source))); + // If there are no sources, blame it on not having any items that can place the entity. + // (Map-generated entities with no locations got a zero-element list in the `if (mapGenerated)` test.) + if (sources.Count == 0) { + sources.Add(([], DependencyNode.Flags.ItemToPlace)); } if (energy != null) { - return DependencyNode.RequireAll(collector[0] /* fuel */, DependencyNode.RequireAny(nonFuel)); + return DependencyNode.RequireAll((energy.fuels, DependencyNode.Flags.Fuel), DependencyNode.RequireAny(sources)); } else { // Doesn't require fuel - return DependencyNode.RequireAny(nonFuel); + return DependencyNode.RequireAny(sources); } } @@ -724,12 +709,12 @@ public static Quality MaxAccessible { public override DependencyNode GetDependencies() { List collector = []; - collector.Add(DependencyNode.Create(new(technologyUnlock, DependencyList.Flags.TechnologyUnlock))); + collector.Add((technologyUnlock, DependencyNode.Flags.TechnologyUnlock)); if (previousQuality != null) { - collector.Add(DependencyNode.Create(new([previousQuality], DependencyList.Flags.Source))); + collector.Add(([previousQuality], DependencyNode.Flags.Source)); } if (level != 0) { - collector.Add(DependencyNode.Create(new(Database.allModules.Where(m => m.moduleSpecification.baseQuality > 0).ToArray(), DependencyList.Flags.Source))); + collector.Add((Database.allModules.Where(m => m.moduleSpecification.baseQuality > 0), DependencyNode.Flags.Source)); } return DependencyNode.RequireAll(collector); } @@ -902,32 +887,32 @@ public class Technology : RecipeOrTechnology { // Technology is very similar to /// internal Lazy> getTriggerEntities { get; set; } = new Lazy>(() => []); - protected override List GetDependenciesHelper() { - List lists = base.GetDependenciesHelper(); + protected override List GetDependenciesHelper() { + List nodes = base.GetDependenciesHelper(); if (prerequisites.Length > 0) { - lists.Add(new(prerequisites, DependencyList.Flags.TechnologyPrerequisites)); + nodes.Add((prerequisites, DependencyNode.Flags.TechnologyPrerequisites)); } if (flags.HasFlag(RecipeFlags.HasResearchTriggerMineEntity)) { // If we have a mining mechanic, use that as the source; otherwise just use the entity. var sources = triggerEntities.Select(e => Database.mechanics.all.SingleOrDefault(m => m.source == e) ?? (FactorioObject)e); - lists.Add(new(sources, DependencyList.Flags.Source)); + nodes.Add((sources, DependencyNode.Flags.Source)); } if (flags.HasFlag(RecipeFlags.HasResearchTriggerBuildEntity)) { - lists.Add(new(triggerEntities, DependencyList.Flags.Source)); + nodes.Add((triggerEntities, DependencyNode.Flags.Source)); } if (flags.HasFlag(RecipeFlags.HasResearchTriggerCreateSpacePlatform)) { var items = Database.items.all.Where(i => i.factorioType == "space-platform-starter-pack"); - lists.Add(new([.. items.Select(i => Database.objectsByTypeName["Mechanics.launch." + i.name])], DependencyList.Flags.Source)); + nodes.Add((items.Select(i => Database.objectsByTypeName["Mechanics.launch." + i.name]), DependencyNode.Flags.Source)); } if (flags.HasFlag(RecipeFlags.HasResearchTriggerSendToOrbit)) { - lists.Add(new([Database.objectsByTypeName["Mechanics.launch." + triggerItem]], DependencyList.Flags.Source)); + nodes.Add(([Database.objectsByTypeName["Mechanics.launch." + triggerItem]], DependencyNode.Flags.Source)); } if (hidden && !enabled) { - lists.Add(new(Array.Empty(), DependencyList.Flags.Hidden)); + nodes.Add(([], DependencyNode.Flags.Hidden)); } - return lists; + return nodes; } } diff --git a/Yafc.Parser/Data/FactorioDataDeserializer_Context.cs b/Yafc.Parser/Data/FactorioDataDeserializer_Context.cs index 49b4a859..c11bffcb 100644 --- a/Yafc.Parser/Data/FactorioDataDeserializer_Context.cs +++ b/Yafc.Parser/Data/FactorioDataDeserializer_Context.cs @@ -402,7 +402,7 @@ private void CalculateMaps(bool netProduction) { } if (plantResults.TryGetValue(item, out string? plantResultName)) { item.plantResult = GetObject(plantResultName); - entityPlacers.Add(GetObject(plantResultName), item); + entityPlacers.Add(GetObject(plantResultName), item, true); } if (item.fuelResult != null) { miscSources.Add(item.fuelResult, item); diff --git a/Yafc.Parser/Data/FactorioDataDeserializer_Entity.cs b/Yafc.Parser/Data/FactorioDataDeserializer_Entity.cs index 391c883b..ad791d43 100644 --- a/Yafc.Parser/Data/FactorioDataDeserializer_Entity.cs +++ b/Yafc.Parser/Data/FactorioDataDeserializer_Entity.cs @@ -561,8 +561,6 @@ void parseEffect(LuaTable effect) { } if (table.Get("autoplace", out LuaTable? generation)) { - entity.mapGenerated = true; - if (generation.Get("probability_expression", out LuaTable? prob)) { float probability = EstimateNoiseExpression(prob); float richness = generation.Get("richness_expression", out LuaTable? rich) ? EstimateNoiseExpression(rich) : probability; @@ -575,7 +573,10 @@ void parseEffect(LuaTable effect) { float estimatedAmount = coverage * (richBase + richMultiplier + (richMultiplierDist * EstimationDistanceFromCenter)); entity.mapGenDensity = estimatedAmount; } - entity.autoplaceControl = generation?.Get("control"); + if (generation.Get("control", out string? control)) { + entity.mapGenerated = true; + entity.autoplaceControl = generation?.Get("control"); + } } entity.loot ??= []; diff --git a/Yafc/Windows/DependencyExplorer.cs b/Yafc/Windows/DependencyExplorer.cs index c4a79c13..917609e7 100644 --- a/Yafc/Windows/DependencyExplorer.cs +++ b/Yafc/Windows/DependencyExplorer.cs @@ -14,19 +14,19 @@ public class DependencyExplorer : PseudoScreen { private readonly List history = []; private FactorioObject current; - private static readonly Dictionary dependencyListTexts = new Dictionary() + private static readonly Dictionary dependencyListTexts = new Dictionary() { - {DependencyList.Flags.Fuel, ("Fuel", "There is no fuel to power this entity")}, - {DependencyList.Flags.Ingredient, ("Ingredient", "There are no ingredients to this recipe")}, - {DependencyList.Flags.IngredientVariant, ("Ingredient", "There are no ingredient variants for this recipe")}, - {DependencyList.Flags.CraftingEntity, ("Crafter", "There are no crafters that can craft this item")}, - {DependencyList.Flags.Source, ("Source", "This item have no sources")}, - {DependencyList.Flags.TechnologyUnlock, ("Research", "This recipe is disabled and there are no technologies to unlock it")}, - {DependencyList.Flags.TechnologyPrerequisites, ("Research", "There are no technology prerequisites")}, - {DependencyList.Flags.ItemToPlace, ("Item", "This entity cannot be placed")}, - {DependencyList.Flags.SourceEntity, ("Source", "This recipe requires another entity")}, - {DependencyList.Flags.Hidden, ("", "This technology is hidden")}, - {DependencyList.Flags.Location, ("Location", "There are no locations that spawn this entity")}, + {DependencyNode.Flags.Fuel, ("Fuel", "There is no fuel to power this entity")}, + {DependencyNode.Flags.Ingredient, ("Ingredient", "There are no ingredients to this recipe")}, + {DependencyNode.Flags.IngredientVariant, ("Ingredient", "There are no ingredient variants for this recipe")}, + {DependencyNode.Flags.CraftingEntity, ("Crafter", "There are no crafters that can craft this item")}, + {DependencyNode.Flags.Source, ("Source", "This item have no sources")}, + {DependencyNode.Flags.TechnologyUnlock, ("Research", "This recipe is disabled and there are no technologies to unlock it")}, + {DependencyNode.Flags.TechnologyPrerequisites, ("Research", "There are no technology prerequisites")}, + {DependencyNode.Flags.ItemToPlace, ("Item", "This entity cannot be placed")}, + {DependencyNode.Flags.SourceEntity, ("Source", "This recipe requires another entity")}, + {DependencyNode.Flags.Hidden, ("", "This technology is hidden")}, + {DependencyNode.Flags.Location, ("Location", "There are no locations that spawn this entity")}, }; public DependencyExplorer(FactorioObject current) : base(60f) { @@ -52,17 +52,17 @@ private void DrawFactorioObject(ImGui gui, FactorioId id) { private void DrawDependencies(ImGui gui) { gui.spacing = 0f; - Dependencies.dependencyList[current].Draw(gui, (gui, data) => { - if (!dependencyListTexts.TryGetValue(data.flags, out var dependencyType)) { - dependencyType = (data.flags.ToString(), "Missing " + data.flags); + Dependencies.dependencyList[current].Draw(gui, (gui, elements, flags) => { + if (!dependencyListTexts.TryGetValue(flags, out var dependencyType)) { + dependencyType = (flags.ToString(), "Missing " + flags); } - if (data.elements.Length > 0) { + if (elements.Count > 0) { gui.AllocateSpacing(0.5f); - if (data.elements.Length == 1) { + if (elements.Count == 1) { gui.BuildText("Require this " + dependencyType.name + ":"); } - else if (data.flags.HasFlags(DependencyList.Flags.RequireEverything)) { + else if (flags.HasFlags(DependencyNode.Flags.RequireEverything)) { gui.BuildText("Require ALL of these " + dependencyType.name + "s:"); } else { @@ -70,7 +70,7 @@ private void DrawDependencies(ImGui gui) { } gui.AllocateSpacing(0.5f); - foreach (var id in data.elements.OrderByDescending(x => CostAnalysis.Instance.flow[x])) { + foreach (var id in elements.OrderByDescending(x => CostAnalysis.Instance.flow[x])) { DrawFactorioObject(gui, id); } } diff --git a/changelog.txt b/changelog.txt index f08d8b90..57d9977d 100644 --- a/changelog.txt +++ b/changelog.txt @@ -22,6 +22,8 @@ Date: Fixes: - "Map generated" entities that don't generate in any locations could break automation analysis. - Recipes that are referenced without being defined do not prevent YAFC from loading. + - (SA) Tree seeds used by the agricultural tower no longer appear twice in the Dependency Explorer. + - Milestone analysis got slower in 2.4.0; increase its speed. ---------------------------------------------------------------------------------------------------------------------- Version: 2.4.0 Date: November 21st 2024