Skip to content

Commit

Permalink
Space age biter captivity (#347)
Browse files Browse the repository at this point in the history
Closes #315; this treats ammo items that produce capture bots as
placement items for the captured spawners, and reads the
CaptureSpawnerTechnologyTrigger to display its information in the
tooltips.

I ended up not reading CaptureRobotPrototype at all; it doesn't contain
interesting information about what the capture robot does. The
interesting information is the chain of triggers from an ammo item to a
projectile entity to a capture bot entity, and also the (non-trigger)
chain from the ammo item to the valid targets to the capture result.

I briefly considered trying to tie accessibility of captured spawners to
both capture rockets and spawners, but (1) the dependency analyzer
doesn't support "... or (A and B)" tests and (2) biter and spitter
spawners are always accessible anyway.
  • Loading branch information
shpaass authored Nov 7, 2024
2 parents 28fe74b + 675f9ab commit f59daae
Show file tree
Hide file tree
Showing 8 changed files with 149 additions and 6 deletions.
30 changes: 28 additions & 2 deletions Yafc.Model/Data/DataClasses.cs
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@ public enum RecipeFlags {
ScaleProductionWithPower = 1 << 3,
/// <summary>Set when the technology has a research trigger to craft an item</summary>
HasResearchTriggerCraft = 1 << 4,
/// <summary>Set when the technology has a research trigger to capture a spawner</summary>
HasResearchTriggerCaptureEntity = 1 << 8,
}

public abstract class RecipeOrTechnology : FactorioObject {
Expand Down Expand Up @@ -326,7 +328,7 @@ public class Item : Goods {
/// </summary>
/// <remarks>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.</remarks>
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; }
Expand All @@ -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<string> projectileNames { get; } = [];
internal HashSet<string>? 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.
Expand Down Expand Up @@ -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";
Expand All @@ -431,7 +439,7 @@ public override void GetDependencies(IDependencyCollector collector, List<Factor
return;
}

collector.Add(itemsToPlace, DependencyList.Flags.ItemToPlace);
collector.Add([.. itemsToPlace, .. miscSources], DependencyList.Flags.ItemToPlace);
}
}

Expand Down Expand Up @@ -488,6 +496,14 @@ public float baseCraftingSpeed {
public EffectReceiver effectReceiver { get; internal set; } = null!;
}

internal class EntityProjectile : Entity {
internal HashSet<string> placeEntities { get; } = [];
}

internal class EntitySpawner : Entity {
internal string? capturedEntityName { get; set; }
}

public sealed class Quality : FactorioObject {
public static Quality Normal { get; internal set; } = null!;
/// <summary>
Expand Down Expand Up @@ -676,6 +692,16 @@ public class Technology : RecipeOrTechnology { // Technology is very similar to
public Dictionary<Recipe, float> changeRecipeProductivity { get; internal set; } = [];
internal override FactorioObjectSortOrder sortingOrder => FactorioObjectSortOrder.Technologies;
public override string type => "Technology";
/// <summary>
/// If the technology has a trigger that requires entities, they are stored here.
/// </summary>
/// <remarks>Lazy-loaded so the database can load and correctly type (eg EntityCrafter, EntitySpawner, etc.) the entities without having to do another pass.</remarks>
public IReadOnlyList<Entity> triggerEntities => getTriggerEntities.Value;

/// <summary>
/// Sets the value used to construct <see cref="triggerEntities"/>.
/// </summary>
internal Lazy<IReadOnlyList<Entity>> getTriggerEntities { get; set; } = new Lazy<IReadOnlyList<Entity>>(() => []);

public override void GetDependencies(IDependencyCollector collector, List<FactorioObject> temp) {
base.GetDependencies(collector, temp);
Expand Down
17 changes: 17 additions & 0 deletions Yafc.Parser/Data/DataParserUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,23 @@ public static T Get<T>(this LuaTable table, int key, T def) {
}

public static IEnumerable<T> ArrayElements<T>(this LuaTable? table) => table?.ArrayElements.OfType<T>() ?? [];

/// <summary>
/// Reads a <see cref="LuaTable"/> that has the format "Thing or array[Thing]", and calls <paramref name="action"/> for each Thing in the array,
/// or for the passed Thing, as appropriate.
/// </summary>
/// <param name="table">A <see cref="LuaTable"/> that might be either an object or an array of objects.</param>
/// <param name="action">The action to perform on each object in <paramref name="table"/>.</param>
public static void ReadObjectOrArray(this LuaTable table, Action<LuaTable> action) {
if (table.ArrayElements.Count > 0) {
foreach (LuaTable entry in table.ArrayElements.OfType<LuaTable>()) {
action(entry);
}
}
else {
action(table);
}
}
}

public static class SpecialNames {
Expand Down
24 changes: 22 additions & 2 deletions Yafc.Parser/Data/FactorioDataDeserializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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++) {
Expand Down Expand Up @@ -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<Item, Module>(name);
var effect = ParseEffect(moduleEffect);
module.moduleSpecification = new ModuleSpecification {
Expand All @@ -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<Item, Ammo>(name);
ammo_type.ReadObjectOrArray(readAmmoType);

if (ammo_type["target_filter"] is LuaTable targets) {
ammo.targetFilter = new(targets.ArrayElements.OfType<string>());
}

void readAmmoType(LuaTable table) {
if (table["action"] is LuaTable action) {
action.ReadObjectOrArray(readTrigger);
}
}
void readTrigger(LuaTable table) {
if (table.Get<string>("type") == "direct" && table["action_delivery"] is LuaTable delivery && delivery.Get<string>("type") == "projectile") {
ammo.projectileNames.Add(delivery.Get<string>("projectile")!);
}
}
}

Item item = DeserializeCommon<Item>(table, "item");

Expand Down
17 changes: 17 additions & 0 deletions Yafc.Parser/Data/FactorioDataDeserializer_Context.cs
Original file line number Diff line number Diff line change
Expand Up @@ -582,6 +582,23 @@ private Recipe CreateSpecialRecipe(FactorioObject production, string category, s
return recipe;
}

private void ParseCaptureEffects() {
HashSet<string> captureRobots = new(allObjects.Where(e => e.factorioType == "capture-robot").Select(e => e.name));
// Projectiles that create capture robots.
HashSet<string> captureProjectiles = new(allObjects.OfType<EntityProjectile>().Where(p => p.placeEntities.Intersect(captureRobots).Any()).Select(p => p.name));
// Ammo that creates projectiles that create capture robots.
List<Ammo> captureAmmo = [.. allObjects.OfType<Ammo>().Where(a => captureProjectiles.Intersect(a.projectileNames).Any())];

Dictionary<string, Entity> entities = allObjects.OfType<Entity>().ToDictionary(e => e.name);
foreach (Ammo ammo in captureAmmo) {
foreach (EntitySpawner spawner in allObjects.OfType<EntitySpawner>()) {
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<TKey, TValue> : IEqualityComparer<List<TValue>> where TKey : notnull where TValue : notnull {
private readonly Dictionary<TKey, IList<TValue>> storage = [];
/// <summary>This function provides a default list of values for the key for when the key is not present in the storage.</summary>
Expand Down
27 changes: 27 additions & 0 deletions Yafc.Parser/Data/FactorioDataDeserializer_Entity.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Entity, EntityProjectile>(name);
if (table["action"] is LuaTable actions) {
actions.ReadObjectOrArray(parseAction);
}

void parseAction(LuaTable action) {
if (action.Get<string>("type") == "direct" && action["action_delivery"] is LuaTable delivery) {
delivery.ReadObjectOrArray(parseDelivery);
}
}
void parseDelivery(LuaTable delivery) {
if (delivery.Get<string>("type") == "instant" && delivery["target_effects"] is LuaTable effects) {
effects.ReadObjectOrArray(parseEffect);
}
}
void parseEffect(LuaTable effect) {
if (effect.Get<string>("type") == "create-entity" && effect.Get("entity_name", out string? createdEntity)) {
projectile.placeEntities.Add(createdEntity);
}
}

break;
case "unit-spawner":
var spawner = GetObject<Entity, EntitySpawner>(name);
spawner.capturedEntityName = table.Get<string>("captured_spawner_entity");
break;
}

Expand Down
12 changes: 12 additions & 0 deletions Yafc.Parser/Data/FactorioDataDeserializer_RecipeAndTechnology.cs
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,18 @@ private void LoadResearchTrigger(LuaTable researchTriggerTable, ref Technology t
technology.ingredients = [new Ingredient(GetObject<Item>(craftItemName), craftCount)];
technology.flags = RecipeFlags.HasResearchTriggerCraft;

break;
case "capture-spawner":
technology.flags = RecipeFlags.HasResearchTriggerCaptureEntity;
if (researchTriggerTable.Get<string>("entity") is string entity) {
technology.getTriggerEntities = new(() => [((Entity)Database.objectsByTypeName["Entity." + entity])]);
}
else {
technology.getTriggerEntities = new(static () =>
Database.entities.all.OfType<EntitySpawner>()
.Where(e => e.capturedEntityName != null)
.ToList());
}
break;
default:
errorCollector.Error($"Research trigger of {technology.typeDotName} has an unsupported type {type}", ErrorSeverity.MinorDataLoss);
Expand Down
22 changes: 20 additions & 2 deletions Yafc/Widgets/ObjectTooltip.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand All @@ -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");
Expand Down
6 changes: 6 additions & 0 deletions changelog.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down

0 comments on commit f59daae

Please sign in to comment.