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: