diff --git a/Yafc.Model/Data/DataClasses.cs b/Yafc.Model/Data/DataClasses.cs index 5e28fad0..61fd4697 100644 --- a/Yafc.Model/Data/DataClasses.cs +++ b/Yafc.Model/Data/DataClasses.cs @@ -87,6 +87,8 @@ public enum RecipeFlags { ScaleProductionWithPower = 1 << 3, /// Set when the technology has a research trigger to craft an item HasResearchTriggerCraft = 1 << 4, + /// Set when the technology has a research trigger to capture a spawner + HasResearchTriggerCaptureEntity = 1 << 8, } public abstract class RecipeOrTechnology : FactorioObject { @@ -326,7 +328,7 @@ public class Item : Goods { /// /// This forces modules to be loaded before other items, since deserialization otherwise creates Item objects for all spoil results. /// It does not protect against modules that spoil into other modules, but one hopes people won't do that. - internal static string[] ExplicitPrototypeLoadOrder { get; } = ["module"]; + internal static string[] ExplicitPrototypeLoadOrder { get; } = ["ammo", "module"]; public Item? fuelResult { get; internal set; } public int stackSize { get; internal set; } @@ -347,6 +349,11 @@ public class Module : Item { public ModuleSpecification moduleSpecification { get; internal set; } = null!; // null-forgiving: Initialized by DeserializeItem. } +internal class Ammo : Item { + internal HashSet projectileNames { get; } = []; + internal HashSet? targetFilter { get; set; } +} + public class Fluid : Goods { public override string type => "Fluid"; public string originalName { get; internal set; } = null!; // name without temperature, null-forgiving: Initialized by DeserializeFluid. @@ -418,6 +425,7 @@ public float Power(Quality quality) : basePower; public EntityEnergy energy { get; internal set; } = null!; // TODO: Prove that this is always properly initialized. (Do we need an EntityWithEnergy type?) public Item[] itemsToPlace { get; internal set; } = null!; // null-forgiving: This is initialized in CalculateMaps. + internal FactorioObject[] miscSources { get; set; } = []; public int size { get; internal set; } internal override FactorioObjectSortOrder sortingOrder => FactorioObjectSortOrder.Entities; public override string type => "Entity"; @@ -431,7 +439,7 @@ public override void GetDependencies(IDependencyCollector collector, List placeEntities { get; } = []; +} + +internal class EntitySpawner : Entity { + internal string? capturedEntityName { get; set; } +} + public sealed class Quality : FactorioObject { public static Quality Normal { get; internal set; } = null!; /// @@ -676,6 +692,16 @@ public class Technology : RecipeOrTechnology { // Technology is very similar to public Dictionary changeRecipeProductivity { get; internal set; } = []; internal override FactorioObjectSortOrder sortingOrder => FactorioObjectSortOrder.Technologies; public override string type => "Technology"; + /// + /// If the technology has a trigger that requires entities, they are stored here. + /// + /// Lazy-loaded so the database can load and correctly type (eg EntityCrafter, EntitySpawner, etc.) the entities without having to do another pass. + public IReadOnlyList triggerEntities => getTriggerEntities.Value; + + /// + /// Sets the value used to construct . + /// + internal Lazy> getTriggerEntities { get; set; } = new Lazy>(() => []); public override void GetDependencies(IDependencyCollector collector, List temp) { base.GetDependencies(collector, temp); diff --git a/Yafc.Parser/Data/DataParserUtils.cs b/Yafc.Parser/Data/DataParserUtils.cs index 1a4d2c9e..41d3a638 100644 --- a/Yafc.Parser/Data/DataParserUtils.cs +++ b/Yafc.Parser/Data/DataParserUtils.cs @@ -86,6 +86,23 @@ public static T Get(this LuaTable table, int key, T def) { } public static IEnumerable ArrayElements(this LuaTable? table) => table?.ArrayElements.OfType() ?? []; + + /// + /// Reads a that has the format "Thing or array[Thing]", and calls for each Thing in the array, + /// or for the passed Thing, as appropriate. + /// + /// A that might be either an object or an array of objects. + /// The action to perform on each object in . + public static void ReadObjectOrArray(this LuaTable table, Action action) { + if (table.ArrayElements.Count > 0) { + foreach (LuaTable entry in table.ArrayElements.OfType()) { + action(entry); + } + } + else { + action(table); + } + } } public static class SpecialNames { diff --git a/Yafc.Parser/Data/FactorioDataDeserializer.cs b/Yafc.Parser/Data/FactorioDataDeserializer.cs index d3f50c6d..73610e3a 100644 --- a/Yafc.Parser/Data/FactorioDataDeserializer.cs +++ b/Yafc.Parser/Data/FactorioDataDeserializer.cs @@ -145,10 +145,11 @@ public Project LoadData(string projectPath, LuaTable data, LuaTable prototypes, DeserializePrototypes(raw, (string)prototypeName, DeserializeEntity, progress, errorCollector); } + ParseCaptureEffects(); + ParseModYafcHandles(data["script_enabled"] as LuaTable); progress.Report(("Post-processing", "Computing maps")); // Deterministically sort all objects - allObjects.Sort((a, b) => a.sortingOrder == b.sortingOrder ? string.Compare(a.typeDotName, b.typeDotName, StringComparison.Ordinal) : a.sortingOrder - b.sortingOrder); for (int i = 0; i < allObjects.Count; i++) { @@ -374,8 +375,8 @@ private static EffectReceiver ParseEffectReceiver(LuaTable? table) { } private void DeserializeItem(LuaTable table, ErrorCollector _) { + string name = table.Get("name", ""); if (table.Get("type", "") == "module" && table.Get("effect", out LuaTable? moduleEffect)) { - string name = table.Get("name", ""); Module module = GetObject(name); var effect = ParseEffect(moduleEffect); module.moduleSpecification = new ModuleSpecification { @@ -387,6 +388,25 @@ private void DeserializeItem(LuaTable table, ErrorCollector _) { baseQuality = effect.quality, }; } + else if (table.Get("type", "") == "ammo" && table["ammo_type"] is LuaTable ammo_type) { + Ammo ammo = GetObject(name); + ammo_type.ReadObjectOrArray(readAmmoType); + + if (ammo_type["target_filter"] is LuaTable targets) { + ammo.targetFilter = new(targets.ArrayElements.OfType()); + } + + void readAmmoType(LuaTable table) { + if (table["action"] is LuaTable action) { + action.ReadObjectOrArray(readTrigger); + } + } + void readTrigger(LuaTable table) { + if (table.Get("type") == "direct" && table["action_delivery"] is LuaTable delivery && delivery.Get("type") == "projectile") { + ammo.projectileNames.Add(delivery.Get("projectile")!); + } + } + } Item item = DeserializeCommon(table, "item"); diff --git a/Yafc.Parser/Data/FactorioDataDeserializer_Context.cs b/Yafc.Parser/Data/FactorioDataDeserializer_Context.cs index 71c4a880..4ee38503 100644 --- a/Yafc.Parser/Data/FactorioDataDeserializer_Context.cs +++ b/Yafc.Parser/Data/FactorioDataDeserializer_Context.cs @@ -582,6 +582,23 @@ private Recipe CreateSpecialRecipe(FactorioObject production, string category, s return recipe; } + private void ParseCaptureEffects() { + HashSet captureRobots = new(allObjects.Where(e => e.factorioType == "capture-robot").Select(e => e.name)); + // Projectiles that create capture robots. + HashSet captureProjectiles = new(allObjects.OfType().Where(p => p.placeEntities.Intersect(captureRobots).Any()).Select(p => p.name)); + // Ammo that creates projectiles that create capture robots. + List captureAmmo = [.. allObjects.OfType().Where(a => captureProjectiles.Intersect(a.projectileNames).Any())]; + + Dictionary entities = allObjects.OfType().ToDictionary(e => e.name); + foreach (Ammo ammo in captureAmmo) { + foreach (EntitySpawner spawner in allObjects.OfType()) { + if ((ammo.targetFilter == null || ammo.targetFilter.Contains(spawner.name)) && spawner.capturedEntityName != null) { + entities[spawner.capturedEntityName].miscSources = [.. entities[spawner.capturedEntityName].miscSources.Append(ammo).Distinct()]; + } + } + } + } + private class DataBucket : IEqualityComparer> where TKey : notnull where TValue : notnull { private readonly Dictionary> storage = []; /// This function provides a default list of values for the key for when the key is not present in the storage. diff --git a/Yafc.Parser/Data/FactorioDataDeserializer_Entity.cs b/Yafc.Parser/Data/FactorioDataDeserializer_Entity.cs index 9dd12536..f4c2e8af 100644 --- a/Yafc.Parser/Data/FactorioDataDeserializer_Entity.cs +++ b/Yafc.Parser/Data/FactorioDataDeserializer_Entity.cs @@ -445,6 +445,33 @@ private void DeserializeEntity(LuaTable table, ErrorCollector errorCollector) { Database.constantCombinatorCapacity = table.Get("item_slot_count", 18); } + break; + case "projectile": + var projectile = GetObject(name); + if (table["action"] is LuaTable actions) { + actions.ReadObjectOrArray(parseAction); + } + + void parseAction(LuaTable action) { + if (action.Get("type") == "direct" && action["action_delivery"] is LuaTable delivery) { + delivery.ReadObjectOrArray(parseDelivery); + } + } + void parseDelivery(LuaTable delivery) { + if (delivery.Get("type") == "instant" && delivery["target_effects"] is LuaTable effects) { + effects.ReadObjectOrArray(parseEffect); + } + } + void parseEffect(LuaTable effect) { + if (effect.Get("type") == "create-entity" && effect.Get("entity_name", out string? createdEntity)) { + projectile.placeEntities.Add(createdEntity); + } + } + + break; + case "unit-spawner": + var spawner = GetObject(name); + spawner.capturedEntityName = table.Get("captured_spawner_entity"); break; } diff --git a/Yafc.Parser/Data/FactorioDataDeserializer_RecipeAndTechnology.cs b/Yafc.Parser/Data/FactorioDataDeserializer_RecipeAndTechnology.cs index a70cbe07..695220df 100644 --- a/Yafc.Parser/Data/FactorioDataDeserializer_RecipeAndTechnology.cs +++ b/Yafc.Parser/Data/FactorioDataDeserializer_RecipeAndTechnology.cs @@ -285,6 +285,18 @@ private void LoadResearchTrigger(LuaTable researchTriggerTable, ref Technology t technology.ingredients = [new Ingredient(GetObject(craftItemName), craftCount)]; technology.flags = RecipeFlags.HasResearchTriggerCraft; + break; + case "capture-spawner": + technology.flags = RecipeFlags.HasResearchTriggerCaptureEntity; + if (researchTriggerTable.Get("entity") is string entity) { + technology.getTriggerEntities = new(() => [((Entity)Database.objectsByTypeName["Entity." + entity])]); + } + else { + technology.getTriggerEntities = new(static () => + Database.entities.all.OfType() + .Where(e => e.capturedEntityName != null) + .ToList()); + } break; default: errorCollector.Error($"Research trigger of {technology.typeDotName} has an unsupported type {type}", ErrorSeverity.MinorDataLoss); diff --git a/Yafc/Widgets/ObjectTooltip.cs b/Yafc/Widgets/ObjectTooltip.cs index aa6eab52..3d3ce425 100644 --- a/Yafc/Widgets/ObjectTooltip.cs +++ b/Yafc/Widgets/ObjectTooltip.cs @@ -498,8 +498,10 @@ private static void BuildRecipe(RecipeOrTechnology recipe, ImGui gui) { } private static void BuildTechnology(Technology technology, ImGui gui) { - bool isResearchTriggerCraft = (technology.flags & RecipeFlags.HasResearchTriggerCraft) == RecipeFlags.HasResearchTriggerCraft; - if (!isResearchTriggerCraft) { + bool isResearchTriggerCraft = technology.flags.HasFlag(RecipeFlags.HasResearchTriggerCraft); + bool isResearchTriggerCapture = technology.flags.HasFlag(RecipeFlags.HasResearchTriggerCaptureEntity); + + if (!isResearchTriggerCraft && !isResearchTriggerCapture) { BuildRecipe(technology, gui); } @@ -524,6 +526,22 @@ private static void BuildTechnology(Technology technology, ImGui gui) { _ = gui.BuildFactorioObjectWithAmount(technology.ingredients[0].goods, technology.ingredients[0].amount, ButtonDisplayStyle.ProductionTableUnscaled); } } + else if (isResearchTriggerCapture) { + BuildSubHeader(gui, "Entity capture required"); + using (gui.EnterGroup(contentPadding)) { + if (technology.triggerEntities.Count == 1) { + gui.BuildText("Capture:"); + gui.BuildFactorioObjectButtonWithText(technology.triggerEntities[0]); + + } + else { + gui.BuildText("Capture one of:"); + foreach (var entity in technology.triggerEntities) { + gui.BuildFactorioObjectButtonWithText(entity); + } + } + } + } if (technology.unlockRecipes.Count > 0) { BuildSubHeader(gui, "Unlocks recipes"); diff --git a/changelog.txt b/changelog.txt index 23849691..d633120f 100644 --- a/changelog.txt +++ b/changelog.txt @@ -15,6 +15,12 @@ // Internal changes: // Changes to the code that do not affect the behavior of the program. ---------------------------------------------------------------------------------------------------------------------- +Version: +Date: + Features: + - (SA) Process accessiblilty of captured spawners, which also fixes biter eggs and subsequent Gleba recipes. + - (SA) Add support for the capture-spawner technology trigger. +---------------------------------------------------------------------------------------------------------------------- Version: 2.2.0 Date: November 6th 2024 Features: