Skip to content

Commit be3bbc2

Browse files
authored
Support all technology triggers (#549)
This fixes the load warnings (for the "craft-fluid" and "scripted" `TechnologyTrigger`s) on the [all-planets torture test](https://mods.factorio.com/mod/kry-all-planet-mods). It does not fix the accessibility issues; I'm still pondering how Lignumis can be handled. It also adds the missing quality restrictions for "craft-item" and "build-entity". <details><summary>The display of the only scripted technology would be more interesting if we displayed tags, but that's out of scope for this PR.</summary> <p> <img width="356" height="470" alt="image" src="https://github.com/user-attachments/assets/421a18ab-4976-4ec7-954a-ec69d43b842d" /> The text before the tags are stripped: >Unlock any 2 planetary science packs from the following list: [technology=metallurgic-science-pack] [technology=agricultural-science-pack] [technology=electromagnetic-science-pack] [technology=electrochemical-science-pack] [technology=cerysian-science-pack] [technology=slp-sunpack] [technology=nanite-science-pack] [technology=battlefield-science-pack] [technology=omnia-omnite-processing] [technology=igrys-mineral-science] [technology=rubia-progression-stage1B] [technology=quantum-science-pack] [technology=ring-science-pack] [technology=anomaly-science-pack] [technology=tiberium-mechanical-research] [technology=moshine-tech-ai-trainer] [technology=pelagos-science-pack] </p> </details>
2 parents e482176 + f551956 commit be3bbc2

File tree

7 files changed

+133
-24
lines changed

7 files changed

+133
-24
lines changed

Yafc.Model/Data/DataClasses.cs

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ internal enum FactorioObjectSortOrder {
2727
Tiles,
2828
Qualities,
2929
Locations,
30+
Triggers,
3031
}
3132

3233
public enum FactorioId { }
@@ -100,9 +101,11 @@ public enum RecipeFlags {
100101
HasResearchTriggerCreateSpacePlatform = 1 << 8,
101102
/// <summary>Set when the technology has a research trigger to launch an arbitrary item.</summary>
102103
HasResearchTriggerSendToOrbit = 1 << 9,
104+
/// <summary>Set when the technology has a scripted research trigger.</summary>
105+
HasResearchTriggerScripted = 1 << 10,
103106

104107
HasResearchTriggerMask = HasResearchTriggerCraft | HasResearchTriggerCaptureEntity | HasResearchTriggerMineEntity | HasResearchTriggerBuildEntity
105-
| HasResearchTriggerCreateSpacePlatform | HasResearchTriggerSendToOrbit,
108+
| HasResearchTriggerCreateSpacePlatform | HasResearchTriggerSendToOrbit | HasResearchTriggerScripted,
106109
}
107110

108111
public abstract class RecipeOrTechnology : FactorioObject {
@@ -137,7 +140,10 @@ protected virtual List<DependencyNode> GetDependenciesHelper() {
137140
collector.Add((ingredients, DependencyNode.Flags.Ingredient));
138141
}
139142
}
140-
collector.Add((crafters, DependencyNode.Flags.CraftingEntity));
143+
if (!flags.HasFlagAny(RecipeFlags.HasResearchTriggerMask)) {
144+
// Trigger researches do not require a crafter, and sometimes (fluid crafting & scripted triggers) don't have one
145+
collector.Add((crafters, DependencyNode.Flags.CraftingEntity));
146+
}
141147
if (sourceEntity != null) {
142148
collector.Add(([sourceEntity], DependencyNode.Flags.SourceEntity));
143149
}
@@ -485,6 +491,14 @@ public class Special : Goods {
485491
internal override FactorioObjectSortOrder sortingOrder => FactorioObjectSortOrder.SpecialGoods;
486492
}
487493

494+
internal sealed class ResearchTrigger : FactorioObject {
495+
public ResearchTrigger() => showInExplorers = false;
496+
public override string type => "Research trigger";
497+
498+
internal override FactorioObjectSortOrder sortingOrder => FactorioObjectSortOrder.Triggers;
499+
public override DependencyNode GetDependencies() => DependencyNode.Create([], DependencyNode.Flags.Source);
500+
}
501+
488502
[Flags]
489503
public enum AllowedEffects {
490504
Speed = 1 << 0,
@@ -960,6 +974,8 @@ public class EntityContainer : Entity {
960974
}
961975

962976
public class Technology : RecipeOrTechnology { // Technology is very similar to recipe
977+
private Quality? _triggerMinimumQuality;
978+
963979
public float count { get; internal set; } // TODO support formula count
964980
public Technology[] prerequisites { get; internal set; } = [];
965981
public List<Recipe> unlockRecipes { get; } = [];
@@ -973,7 +989,21 @@ public class Technology : RecipeOrTechnology { // Technology is very similar to
973989
/// </summary>
974990
/// <remarks>Lazy-loaded so the database can load and correctly type (eg EntityCrafter, EntitySpawner, etc.) the entities without having to do another pass.</remarks>
975991
public IReadOnlyList<Entity> triggerEntities => getTriggerEntities.Value;
976-
public Item? triggerItem { get; internal set; }
992+
/// <summary>
993+
/// The object associated with the research trigger. Must not be <see langword="null"/> when <see cref="RecipeFlags.HasResearchTriggerScripted"/>
994+
/// or <see cref="RecipeFlags.HasResearchTriggerSendToOrbit"/>.
995+
/// </summary>
996+
public FactorioObject? triggerObject { get; internal set; }
997+
/// <summary>
998+
/// For entity and item triggers, the minimum quality. Factorio tracks more complicated filters (e.g. &lt; rare), but the only thing we care about
999+
/// for accessibility is the minimum acceptable quality. This is not stored as an <see cref="IObjectWithQuality{T}"/> because those don't exist
1000+
/// when this field is loaded from the lua tables.
1001+
/// </summary>
1002+
[AllowNull]
1003+
public Quality triggerMinimumQuality {
1004+
get => _triggerMinimumQuality ?? Quality.Normal;
1005+
internal set => _triggerMinimumQuality = value;
1006+
}
9771007

9781008
/// <summary>
9791009
/// Sets the value used to construct <see cref="triggerEntities"/>.
@@ -994,12 +1024,21 @@ protected override List<DependencyNode> GetDependenciesHelper() {
9941024
if (flags.HasFlag(RecipeFlags.HasResearchTriggerBuildEntity)) {
9951025
nodes.Add((triggerEntities, DependencyNode.Flags.Source));
9961026
}
1027+
if (flags.HasFlagAny(RecipeFlags.HasResearchTriggerBuildEntity | RecipeFlags.HasResearchTriggerCraft)) {
1028+
if (triggerMinimumQuality > Quality.Normal) {
1029+
nodes.Add(([triggerMinimumQuality], DependencyNode.Flags.Source));
1030+
}
1031+
}
9971032
if (flags.HasFlag(RecipeFlags.HasResearchTriggerCreateSpacePlatform)) {
9981033
var items = Database.items.all.Where(i => i.factorioType == "space-platform-starter-pack");
9991034
nodes.Add((items.Select(i => Database.objectsByTypeName["Mechanics.launch." + i.name]), DependencyNode.Flags.Source));
10001035
}
10011036
if (flags.HasFlag(RecipeFlags.HasResearchTriggerSendToOrbit)) {
1002-
nodes.Add(([Database.objectsByTypeName["Mechanics.launch." + triggerItem]], DependencyNode.Flags.Source));
1037+
nodes.Add(([Database.objectsByTypeName["Mechanics.launch." + triggerObject]], DependencyNode.Flags.Source));
1038+
}
1039+
if (flags.HasFlag(RecipeFlags.HasResearchTriggerScripted)) {
1040+
// null-forgiving: triggerObject is set before setting HasResearchTriggerScripted
1041+
nodes.Add(([triggerObject!], DependencyNode.Flags.Source));
10031042
}
10041043

10051044
if (!enabled) {

Yafc.Parser/Data/FactorioDataDeserializer.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -162,11 +162,12 @@ public Project LoadData(string projectPath, LuaTable data, LuaTable prototypes,
162162
DeserializePrototypes(raw, "planet", DeserializeLocation, progress, errorCollector);
163163
DeserializePrototypes(raw, "space-location", DeserializeLocation, progress, errorCollector);
164164
rootAccessible.Add(GetObject<Location>("nauvis"));
165-
progress.Report((LSs.ProgressLoading, LSs.ProgressLoadingTechnologies));
166-
DeserializePrototypes(raw, "technology", DeserializeTechnology, progress, errorCollector);
167165
progress.Report((LSs.ProgressLoading, LSs.ProgressLoadingQualities));
168166
DeserializePrototypes(raw, "quality", DeserializeQuality, progress, errorCollector);
169167
Quality.Normal = GetObject<Quality>("normal");
168+
// Qualities must be loaded before technologies, to properly initialize Technology.triggerMinimumQuality
169+
progress.Report((LSs.ProgressLoading, LSs.ProgressLoadingTechnologies));
170+
DeserializePrototypes(raw, "technology", DeserializeTechnology, progress, errorCollector);
170171
rootAccessible.Add(Quality.Normal);
171172
DeserializePrototypes(raw, "asteroid-chunk", DeserializeAsteroidChunk, progress, errorCollector);
172173

Yafc.Parser/Data/FactorioDataDeserializer_Context.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -302,7 +302,8 @@ private void ExportBuiltData() {
302302
int firstTile = Skip(firstEntity, FactorioObjectSortOrder.Entities);
303303
int firstQuality = Skip(firstTile, FactorioObjectSortOrder.Tiles);
304304
int firstLocation = Skip(firstQuality, FactorioObjectSortOrder.Qualities);
305-
int last = Skip(firstLocation, FactorioObjectSortOrder.Locations);
305+
int firstTrigger = Skip(firstLocation, FactorioObjectSortOrder.Locations);
306+
int last = Skip(firstTrigger, FactorioObjectSortOrder.Triggers);
306307
if (last != allObjects.Count) {
307308
throw new Exception("Something is not right");
308309
}
@@ -318,7 +319,7 @@ private void ExportBuiltData() {
318319
Database.technologies = new FactorioIdRange<Technology>(firstTechnology, firstEntity, allObjects);
319320
Database.entities = new FactorioIdRange<Entity>(firstEntity, firstTile, allObjects);
320321
Database.qualities = new FactorioIdRange<Quality>(firstQuality, firstLocation, allObjects);
321-
Database.locations = new FactorioIdRange<Location>(firstLocation, last, allObjects);
322+
Database.locations = new FactorioIdRange<Location>(firstLocation, firstTrigger, allObjects);
322323
Database.fluidVariants = fluidVariants;
323324

324325
Database.allModules = [.. allModules];

Yafc.Parser/Data/FactorioDataDeserializer_RecipeAndTechnology.cs

Lines changed: 60 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using System.Diagnostics.CodeAnalysis;
23
using System.Linq;
34
using Yafc.I18n;
45
using Yafc.Model;
@@ -139,7 +140,7 @@ private void LoadTechnologyData(Technology technology, LuaTable table, bool forc
139140
recipeCategories.Add(SpecialNames.Labs, technology);
140141
}
141142
else if (table.Get("research_trigger", out LuaTable? researchTriggerTable)) {
142-
LoadResearchTrigger(researchTriggerTable, ref technology, errorCollector);
143+
LoadResearchTrigger(researchTriggerTable, technology, errorCollector);
143144
technology.ingredients ??= [];
144145
recipeCategories.Add(SpecialNames.TechnologyTrigger, technology);
145146
}
@@ -319,19 +320,29 @@ private Ingredient[] LoadResearchIngredientList(LuaTable table) {
319320
}).Where(x => x is not null).ToArray() ?? [];
320321
}
321322

322-
private void LoadResearchTrigger(LuaTable researchTriggerTable, ref Technology technology, ErrorCollector errorCollector) {
323+
private void LoadResearchTrigger(LuaTable researchTriggerTable, Technology technology, ErrorCollector errorCollector) {
323324
if (!researchTriggerTable.Get("type", out string? type)) {
324325
errorCollector.Error($"Research trigger of {technology.typeDotName} does not have a type field", ErrorSeverity.MinorDataLoss);
325326
return;
326327
}
327328

328329
switch (type) {
329-
case "craft-item":
330-
if (!researchTriggerTable.Get("item", out string? craftItemName)) {
331-
errorCollector.Error($"Research trigger {type} of {technology.typeDotName} does not have an item field", ErrorSeverity.MinorDataLoss);
330+
case "craft-fluid":
331+
if (!researchTriggerTable.Get("fluid", out string? craftFluidName)) {
332+
errorCollector.Error($"Research trigger {type} of {technology.typeDotName} does not have a fluid field", ErrorSeverity.MinorDataLoss);
332333
break;
333334
}
334335
float craftCount = researchTriggerTable.Get("count", 1);
336+
technology.ingredients = [new Ingredient(GetObject<Fluid>(craftFluidName), craftCount)];
337+
technology.flags = RecipeFlags.HasResearchTriggerCraft;
338+
339+
break;
340+
case "craft-item":
341+
if (!loadQualityFromFilter(technology, researchTriggerTable, "item", out string? craftItemName)) {
342+
errorCollector.Error($"Research trigger {type} of {technology.typeDotName} does not have a recognized item field", ErrorSeverity.MinorDataLoss);
343+
break;
344+
}
345+
craftCount = researchTriggerTable.Get("count", 1);
335346
technology.ingredients = [new Ingredient(GetObject<Item>(craftItemName), craftCount)];
336347
technology.flags = RecipeFlags.HasResearchTriggerCraft;
337348

@@ -358,13 +369,11 @@ private void LoadResearchTrigger(LuaTable researchTriggerTable, ref Technology t
358369
break;
359370
case "build-entity":
360371
technology.flags = RecipeFlags.HasResearchTriggerBuildEntity;
361-
if (researchTriggerTable.Get("entity", out entity)
362-
|| (researchTriggerTable.Get("entity", out LuaTable? entityFilter) && entityFilter.Get("name", out entity))) {
363-
372+
if (loadQualityFromFilter(technology, researchTriggerTable, "entity", out entity)) {
364373
technology.getTriggerEntities = new(() => [((Entity)Database.objectsByTypeName["Entity." + entity])]);
365374
}
366375
else {
367-
errorCollector.Error($"Research trigger {type} of {technology.typeDotName} does not have an entity field", ErrorSeverity.MinorDataLoss);
376+
errorCollector.Error($"Research trigger {type} of {technology.typeDotName} does not have a recognized entity field", ErrorSeverity.MinorDataLoss);
368377
}
369378

370379
break;
@@ -377,15 +386,56 @@ private void LoadResearchTrigger(LuaTable researchTriggerTable, ref Technology t
377386
break;
378387
}
379388
Item item = GetObject<Item>(itemName);
380-
technology.triggerItem = item;
389+
technology.triggerObject = item;
381390
technology.flags |= RecipeFlags.HasResearchTriggerSendToOrbit;
382391
// Create a launch recipe for items without a `rocket_launch_products`, without altering existing launch recipes.
383392
EnsureLaunchRecipe(item, null);
384393
break;
394+
case "scripted":
395+
researchTriggerTable["name"] = technology.name;
396+
researchTriggerTable["localised_description"] = researchTriggerTable["trigger_description"];
397+
ResearchTrigger trigger = DeserializeCommon<ResearchTrigger>(researchTriggerTable, "trigger");
398+
trigger.locName = LSs.TechnologyTrigger.L(technology.locName ?? technology.name);
399+
technology.triggerObject = trigger;
400+
technology.flags |= RecipeFlags.HasResearchTriggerScripted;
401+
rootAccessible.Add(trigger);
402+
break;
385403
default:
386404
errorCollector.Error(LSs.ResearchHasAnUnsupportedTriggerType.L(technology.typeDotName, type), ErrorSeverity.MinorDataLoss);
387405
break;
388406
}
407+
408+
bool loadQualityFromFilter(Technology technology, LuaTable trigger, string key, [NotNullWhen(true)] out string? objectName) {
409+
if (trigger.Get(key, out objectName)) {
410+
// Basic string value
411+
return true;
412+
}
413+
414+
if (trigger.Get(key, out LuaTable? filter) && filter.Get("name", out objectName)) {
415+
// Load the quality specifiers from the table-based filter
416+
if (!registeredObjects.TryGetValue((typeof(Quality), filter.Get<string>("quality")), out var quality)) {
417+
return true;
418+
}
419+
420+
switch (filter.Get<string>("comparator")) {
421+
// `≠ normal` is the same as `> normal`. `≠ anything-else` is the same as `= normal` for dependency analysis.
422+
case "!=" or "≠" when quality == Quality.Normal:
423+
case ">":
424+
// This filter requires at least the quality after the specified quality.
425+
// I expect `> legendary` is a load error in Factorio, so treating it as "any quality" here should be fine.
426+
technology.triggerMinimumQuality = ((Quality)quality).nextQuality;
427+
break;
428+
case ">=" or "≥" or "=":
429+
technology.triggerMinimumQuality = (Quality)quality;
430+
break;
431+
default:
432+
// Nothing special: Normal quality is acceptable, no quality, or no comparator
433+
break;
434+
}
435+
return true;
436+
}
437+
return false;
438+
}
389439
}
390440

391441
private void LoadRecipeData(Recipe recipe, LuaTable table, bool forceDisable, ErrorCollector errorCollector) {

Yafc/Data/locale/en/yafc.cfg

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,11 +110,12 @@ tooltip-header-allowed-modules=Allowed module__plural_for_parameter__1__{1=|rest
110110
tooltip-header-unlocked-by-technologies=Unlocked by__plural_for_parameter__1__{rest=}__
111111
technology-is-disabled=This technology is disabled and cannot be researched.
112112
tooltip-header-technology-prerequisites=Prerequisite__plural_for_parameter__1__{1=|rest=s}__
113-
tooltip-header-technology-item-crafting=Item crafting required
113+
tooltip-header-technology-crafting=Crafting required
114114
tooltip-header-technology-capture=Capture __plural_for_parameter__1__{1=this|rest=any}__ entity
115115
tooltip-header-technology-mine-entity=Mine __plural_for_parameter__1__{1=this|rest=any}__ entity
116116
tooltip-header-technology-build-entity=Build __plural_for_parameter__1__{1=this|rest=any}__ entity
117117
tooltip-header-technology-launch-item=Launch __plural_for_parameter__1__{1=this|rest=any}__ item
118+
tooltip-header-technology-scripted=Satisfy scripted requirements
118119
tooltip-header-unlocks-recipes=Unlocks recipe__plural_for_parameter__1__{1=|rest=s}__
119120
tooltip-header-unlocks-locations=Unlocks location__plural_for_parameter__1__{1=|rest=s}__
120121
tooltip-header-total-science-required=Total science required
@@ -524,6 +525,8 @@ product-fixed-spoilage=__1__, __2__ spoiled
524525
temperature=__1__°
525526
temperature-range=__1__°-__2__°
526527
528+
technology-trigger=__1__ trigger
529+
527530
; DataUtils.cs
528531
ctrl-click-hint-complete-milestones=Hint: Complete milestones to enable ctrl+click
529532
ctrl-click-hint-mark-accessible=Hint: Mark a recipe as accessible to enable ctrl+click

Yafc/Widgets/ObjectTooltip.cs

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ private static void BuildIconRow(ImGui gui, IReadOnlyList<IFactorioObjectWrapper
145145

146146
private static void BuildItem(ImGui gui, IFactorioObjectWrapper item, string? extraText = null) {
147147
using (gui.EnterRow()) {
148-
gui.BuildFactorioObjectIcon(item.target);
148+
gui.BuildFactorioObjectIcon(item);
149149
gui.BuildText(item.text + extraText, TextBlockDisplayStyle.WrappedText);
150150
}
151151
}
@@ -584,6 +584,7 @@ private static void BuildTechnology(Technology technology, ImGui gui) {
584584
bool isResearchTriggerBuild = technology.flags.HasFlag(RecipeFlags.HasResearchTriggerBuildEntity);
585585
bool isResearchTriggerPlatform = technology.flags.HasFlag(RecipeFlags.HasResearchTriggerCreateSpacePlatform);
586586
bool isResearchTriggerLaunch = technology.flags.HasFlag(RecipeFlags.HasResearchTriggerSendToOrbit);
587+
bool isResearchTriggerScripted = technology.flags.HasFlag(RecipeFlags.HasResearchTriggerScripted);
587588

588589
if (!technology.flags.HasFlagAny(RecipeFlags.HasResearchTriggerMask)) {
589590
BuildRecipe(technology, gui);
@@ -603,11 +604,12 @@ private static void BuildTechnology(Technology technology, ImGui gui) {
603604
}
604605

605606
if (isResearchTriggerCraft) {
606-
BuildSubHeader(gui, LSs.TooltipHeaderTechnologyItemCrafting);
607+
BuildSubHeader(gui, LSs.TooltipHeaderTechnologyCrafting);
607608
using (gui.EnterGroup(contentPadding)) {
608609
using var grid = gui.EnterInlineGrid(3f);
609610
grid.Next();
610-
_ = gui.BuildFactorioObjectWithAmount(technology.ingredients[0].goods, technology.ingredients[0].amount, ButtonDisplayStyle.ProductionTableUnscaled);
611+
_ = gui.BuildFactorioObjectWithAmount(technology.ingredients[0].goods.With(technology.triggerMinimumQuality),
612+
technology.ingredients[0].amount, ButtonDisplayStyle.ProductionTableUnscaled);
611613
}
612614
}
613615
else if (isResearchTriggerCapture) {
@@ -625,7 +627,7 @@ private static void BuildTechnology(Technology technology, ImGui gui) {
625627
else if (isResearchTriggerBuild) {
626628
BuildSubHeader(gui, LSs.TooltipHeaderTechnologyBuildEntity.L(technology.triggerEntities.Count));
627629
using (gui.EnterGroup(contentPadding)) {
628-
BuildIconRow(gui, technology.triggerEntities, 2);
630+
BuildIconRow(gui, [.. technology.triggerEntities.Select(e => e.With(technology.triggerMinimumQuality))], 2);
629631
}
630632
}
631633
else if (isResearchTriggerPlatform) {
@@ -638,7 +640,14 @@ private static void BuildTechnology(Technology technology, ImGui gui) {
638640
else if (isResearchTriggerLaunch) {
639641
BuildSubHeader(gui, LSs.TooltipHeaderTechnologyLaunchItem.L(1));
640642
using (gui.EnterGroup(contentPadding)) {
641-
gui.BuildFactorioObjectButtonWithText(technology.triggerItem);
643+
gui.BuildFactorioObjectButtonWithText(technology.triggerObject);
644+
}
645+
}
646+
else if (isResearchTriggerScripted) {
647+
BuildSubHeader(gui, LSs.TooltipHeaderTechnologyScripted);
648+
using (gui.EnterGroup(contentPadding)) {
649+
// null-forgiving: triggerObject is set before setting HasResearchTriggerScripted
650+
gui.BuildText(technology.triggerObject!.locDescr, TextBlockDisplayStyle.WrappedText);
642651
}
643652
}
644653

0 commit comments

Comments
 (0)