Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 43 additions & 4 deletions Yafc.Model/Data/DataClasses.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ internal enum FactorioObjectSortOrder {
Tiles,
Qualities,
Locations,
Triggers,
}

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

HasResearchTriggerMask = HasResearchTriggerCraft | HasResearchTriggerCaptureEntity | HasResearchTriggerMineEntity | HasResearchTriggerBuildEntity
| HasResearchTriggerCreateSpacePlatform | HasResearchTriggerSendToOrbit,
| HasResearchTriggerCreateSpacePlatform | HasResearchTriggerSendToOrbit | HasResearchTriggerScripted,
}

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

internal sealed class ResearchTrigger : FactorioObject {
public ResearchTrigger() => showInExplorers = false;
public override string type => "Research trigger";

internal override FactorioObjectSortOrder sortingOrder => FactorioObjectSortOrder.Triggers;
public override DependencyNode GetDependencies() => DependencyNode.Create([], DependencyNode.Flags.Source);
}

[Flags]
public enum AllowedEffects {
Speed = 1 << 0,
Expand Down Expand Up @@ -960,6 +974,8 @@ public class EntityContainer : Entity {
}

public class Technology : RecipeOrTechnology { // Technology is very similar to recipe
private Quality? _triggerMinimumQuality;

public float count { get; internal set; } // TODO support formula count
public Technology[] prerequisites { get; internal set; } = [];
public List<Recipe> unlockRecipes { get; } = [];
Expand All @@ -973,7 +989,21 @@ public class Technology : RecipeOrTechnology { // Technology is very similar to
/// </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;
public Item? triggerItem { get; internal set; }
/// <summary>
/// The object associated with the research trigger. Must not be <see langword="null"/> when <see cref="RecipeFlags.HasResearchTriggerScripted"/>
/// or <see cref="RecipeFlags.HasResearchTriggerSendToOrbit"/>.
/// </summary>
public FactorioObject? triggerObject { get; internal set; }
/// <summary>
/// For entity and item triggers, the minimum quality. Factorio tracks more complicated filters (e.g. &lt; rare), but the only thing we care about
/// for accessibility is the minimum acceptable quality. This is not stored as an <see cref="IObjectWithQuality{T}"/> because those don't exist
/// when this field is loaded from the lua tables.
/// </summary>
[AllowNull]
public Quality triggerMinimumQuality {
get => _triggerMinimumQuality ?? Quality.Normal;
internal set => _triggerMinimumQuality = value;
}

/// <summary>
/// Sets the value used to construct <see cref="triggerEntities"/>.
Expand All @@ -994,12 +1024,21 @@ protected override List<DependencyNode> GetDependenciesHelper() {
if (flags.HasFlag(RecipeFlags.HasResearchTriggerBuildEntity)) {
nodes.Add((triggerEntities, DependencyNode.Flags.Source));
}
if (flags.HasFlagAny(RecipeFlags.HasResearchTriggerBuildEntity | RecipeFlags.HasResearchTriggerCraft)) {
if (triggerMinimumQuality > Quality.Normal) {
nodes.Add(([triggerMinimumQuality], DependencyNode.Flags.Source));
}
}
if (flags.HasFlag(RecipeFlags.HasResearchTriggerCreateSpacePlatform)) {
var items = Database.items.all.Where(i => i.factorioType == "space-platform-starter-pack");
nodes.Add((items.Select(i => Database.objectsByTypeName["Mechanics.launch." + i.name]), DependencyNode.Flags.Source));
}
if (flags.HasFlag(RecipeFlags.HasResearchTriggerSendToOrbit)) {
nodes.Add(([Database.objectsByTypeName["Mechanics.launch." + triggerItem]], DependencyNode.Flags.Source));
nodes.Add(([Database.objectsByTypeName["Mechanics.launch." + triggerObject]], DependencyNode.Flags.Source));
}
if (flags.HasFlag(RecipeFlags.HasResearchTriggerScripted)) {
// null-forgiving: triggerObject is set before setting HasResearchTriggerScripted
nodes.Add(([triggerObject!], DependencyNode.Flags.Source));
}

if (!enabled) {
Expand Down
5 changes: 3 additions & 2 deletions Yafc.Parser/Data/FactorioDataDeserializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -162,11 +162,12 @@ public Project LoadData(string projectPath, LuaTable data, LuaTable prototypes,
DeserializePrototypes(raw, "planet", DeserializeLocation, progress, errorCollector);
DeserializePrototypes(raw, "space-location", DeserializeLocation, progress, errorCollector);
rootAccessible.Add(GetObject<Location>("nauvis"));
progress.Report((LSs.ProgressLoading, LSs.ProgressLoadingTechnologies));
DeserializePrototypes(raw, "technology", DeserializeTechnology, progress, errorCollector);
progress.Report((LSs.ProgressLoading, LSs.ProgressLoadingQualities));
DeserializePrototypes(raw, "quality", DeserializeQuality, progress, errorCollector);
Quality.Normal = GetObject<Quality>("normal");
// Qualities must be loaded before technologies, to properly initialize Technology.triggerMinimumQuality
progress.Report((LSs.ProgressLoading, LSs.ProgressLoadingTechnologies));
DeserializePrototypes(raw, "technology", DeserializeTechnology, progress, errorCollector);
rootAccessible.Add(Quality.Normal);
DeserializePrototypes(raw, "asteroid-chunk", DeserializeAsteroidChunk, progress, errorCollector);

Expand Down
5 changes: 3 additions & 2 deletions Yafc.Parser/Data/FactorioDataDeserializer_Context.cs
Original file line number Diff line number Diff line change
Expand Up @@ -302,7 +302,8 @@ private void ExportBuiltData() {
int firstTile = Skip(firstEntity, FactorioObjectSortOrder.Entities);
int firstQuality = Skip(firstTile, FactorioObjectSortOrder.Tiles);
int firstLocation = Skip(firstQuality, FactorioObjectSortOrder.Qualities);
int last = Skip(firstLocation, FactorioObjectSortOrder.Locations);
int firstTrigger = Skip(firstLocation, FactorioObjectSortOrder.Locations);
int last = Skip(firstTrigger, FactorioObjectSortOrder.Triggers);
if (last != allObjects.Count) {
throw new Exception("Something is not right");
}
Expand All @@ -318,7 +319,7 @@ private void ExportBuiltData() {
Database.technologies = new FactorioIdRange<Technology>(firstTechnology, firstEntity, allObjects);
Database.entities = new FactorioIdRange<Entity>(firstEntity, firstTile, allObjects);
Database.qualities = new FactorioIdRange<Quality>(firstQuality, firstLocation, allObjects);
Database.locations = new FactorioIdRange<Location>(firstLocation, last, allObjects);
Database.locations = new FactorioIdRange<Location>(firstLocation, firstTrigger, allObjects);
Database.fluidVariants = fluidVariants;

Database.allModules = [.. allModules];
Expand Down
70 changes: 60 additions & 10 deletions Yafc.Parser/Data/FactorioDataDeserializer_RecipeAndTechnology.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Yafc.I18n;
using Yafc.Model;
Expand Down Expand Up @@ -139,7 +140,7 @@ private void LoadTechnologyData(Technology technology, LuaTable table, bool forc
recipeCategories.Add(SpecialNames.Labs, technology);
}
else if (table.Get("research_trigger", out LuaTable? researchTriggerTable)) {
LoadResearchTrigger(researchTriggerTable, ref technology, errorCollector);
LoadResearchTrigger(researchTriggerTable, technology, errorCollector);
technology.ingredients ??= [];
recipeCategories.Add(SpecialNames.TechnologyTrigger, technology);
}
Expand Down Expand Up @@ -319,19 +320,29 @@ private Ingredient[] LoadResearchIngredientList(LuaTable table) {
}).Where(x => x is not null).ToArray() ?? [];
}

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

switch (type) {
case "craft-item":
if (!researchTriggerTable.Get("item", out string? craftItemName)) {
errorCollector.Error($"Research trigger {type} of {technology.typeDotName} does not have an item field", ErrorSeverity.MinorDataLoss);
case "craft-fluid":
if (!researchTriggerTable.Get("fluid", out string? craftFluidName)) {
errorCollector.Error($"Research trigger {type} of {technology.typeDotName} does not have a fluid field", ErrorSeverity.MinorDataLoss);
break;
}
float craftCount = researchTriggerTable.Get("count", 1);
technology.ingredients = [new Ingredient(GetObject<Fluid>(craftFluidName), craftCount)];
technology.flags = RecipeFlags.HasResearchTriggerCraft;

break;
case "craft-item":
if (!loadQualityFromFilter(technology, researchTriggerTable, "item", out string? craftItemName)) {
errorCollector.Error($"Research trigger {type} of {technology.typeDotName} does not have a recognized item field", ErrorSeverity.MinorDataLoss);
break;
}
craftCount = researchTriggerTable.Get("count", 1);
technology.ingredients = [new Ingredient(GetObject<Item>(craftItemName), craftCount)];
technology.flags = RecipeFlags.HasResearchTriggerCraft;

Expand All @@ -358,13 +369,11 @@ private void LoadResearchTrigger(LuaTable researchTriggerTable, ref Technology t
break;
case "build-entity":
technology.flags = RecipeFlags.HasResearchTriggerBuildEntity;
if (researchTriggerTable.Get("entity", out entity)
|| (researchTriggerTable.Get("entity", out LuaTable? entityFilter) && entityFilter.Get("name", out entity))) {

if (loadQualityFromFilter(technology, researchTriggerTable, "entity", out entity)) {
technology.getTriggerEntities = new(() => [((Entity)Database.objectsByTypeName["Entity." + entity])]);
}
else {
errorCollector.Error($"Research trigger {type} of {technology.typeDotName} does not have an entity field", ErrorSeverity.MinorDataLoss);
errorCollector.Error($"Research trigger {type} of {technology.typeDotName} does not have a recognized entity field", ErrorSeverity.MinorDataLoss);
}

break;
Expand All @@ -377,15 +386,56 @@ private void LoadResearchTrigger(LuaTable researchTriggerTable, ref Technology t
break;
}
Item item = GetObject<Item>(itemName);
technology.triggerItem = item;
technology.triggerObject = item;
technology.flags |= RecipeFlags.HasResearchTriggerSendToOrbit;
// Create a launch recipe for items without a `rocket_launch_products`, without altering existing launch recipes.
EnsureLaunchRecipe(item, null);
break;
case "scripted":
researchTriggerTable["name"] = technology.name;
researchTriggerTable["localised_description"] = researchTriggerTable["trigger_description"];
ResearchTrigger trigger = DeserializeCommon<ResearchTrigger>(researchTriggerTable, "trigger");
trigger.locName = LSs.TechnologyTrigger.L(technology.locName ?? technology.name);
technology.triggerObject = trigger;
technology.flags |= RecipeFlags.HasResearchTriggerScripted;
rootAccessible.Add(trigger);
break;
default:
errorCollector.Error(LSs.ResearchHasAnUnsupportedTriggerType.L(technology.typeDotName, type), ErrorSeverity.MinorDataLoss);
break;
}

bool loadQualityFromFilter(Technology technology, LuaTable trigger, string key, [NotNullWhen(true)] out string? objectName) {
if (trigger.Get(key, out objectName)) {
// Basic string value
return true;
}

if (trigger.Get(key, out LuaTable? filter) && filter.Get("name", out objectName)) {
// Load the quality specifiers from the table-based filter
if (!registeredObjects.TryGetValue((typeof(Quality), filter.Get<string>("quality")), out var quality)) {
return true;
}

switch (filter.Get<string>("comparator")) {
// `≠ normal` is the same as `> normal`. `≠ anything-else` is the same as `= normal` for dependency analysis.
case "!=" or "≠" when quality == Quality.Normal:
case ">":
// This filter requires at least the quality after the specified quality.
// I expect `> legendary` is a load error in Factorio, so treating it as "any quality" here should be fine.
technology.triggerMinimumQuality = ((Quality)quality).nextQuality;
break;
case ">=" or "≥" or "=":
technology.triggerMinimumQuality = (Quality)quality;
break;
default:
// Nothing special: Normal quality is acceptable, no quality, or no comparator
break;
}
return true;
}
return false;
}
}

private void LoadRecipeData(Recipe recipe, LuaTable table, bool forceDisable, ErrorCollector errorCollector) {
Expand Down
5 changes: 4 additions & 1 deletion Yafc/Data/locale/en/yafc.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -110,11 +110,12 @@ tooltip-header-allowed-modules=Allowed module__plural_for_parameter__1__{1=|rest
tooltip-header-unlocked-by-technologies=Unlocked by__plural_for_parameter__1__{rest=}__
technology-is-disabled=This technology is disabled and cannot be researched.
tooltip-header-technology-prerequisites=Prerequisite__plural_for_parameter__1__{1=|rest=s}__
tooltip-header-technology-item-crafting=Item crafting required
tooltip-header-technology-crafting=Crafting required
tooltip-header-technology-capture=Capture __plural_for_parameter__1__{1=this|rest=any}__ entity
tooltip-header-technology-mine-entity=Mine __plural_for_parameter__1__{1=this|rest=any}__ entity
tooltip-header-technology-build-entity=Build __plural_for_parameter__1__{1=this|rest=any}__ entity
tooltip-header-technology-launch-item=Launch __plural_for_parameter__1__{1=this|rest=any}__ item
tooltip-header-technology-scripted=Satisfy scripted requirements
tooltip-header-unlocks-recipes=Unlocks recipe__plural_for_parameter__1__{1=|rest=s}__
tooltip-header-unlocks-locations=Unlocks location__plural_for_parameter__1__{1=|rest=s}__
tooltip-header-total-science-required=Total science required
Expand Down Expand Up @@ -524,6 +525,8 @@ product-fixed-spoilage=__1__, __2__ spoiled
temperature=__1__°
temperature-range=__1__°-__2__°

technology-trigger=__1__ trigger

; DataUtils.cs
ctrl-click-hint-complete-milestones=Hint: Complete milestones to enable ctrl+click
ctrl-click-hint-mark-accessible=Hint: Mark a recipe as accessible to enable ctrl+click
Expand Down
19 changes: 14 additions & 5 deletions Yafc/Widgets/ObjectTooltip.cs
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ private static void BuildIconRow(ImGui gui, IReadOnlyList<IFactorioObjectWrapper

private static void BuildItem(ImGui gui, IFactorioObjectWrapper item, string? extraText = null) {
using (gui.EnterRow()) {
gui.BuildFactorioObjectIcon(item.target);
gui.BuildFactorioObjectIcon(item);
gui.BuildText(item.text + extraText, TextBlockDisplayStyle.WrappedText);
}
}
Expand Down Expand Up @@ -584,6 +584,7 @@ private static void BuildTechnology(Technology technology, ImGui gui) {
bool isResearchTriggerBuild = technology.flags.HasFlag(RecipeFlags.HasResearchTriggerBuildEntity);
bool isResearchTriggerPlatform = technology.flags.HasFlag(RecipeFlags.HasResearchTriggerCreateSpacePlatform);
bool isResearchTriggerLaunch = technology.flags.HasFlag(RecipeFlags.HasResearchTriggerSendToOrbit);
bool isResearchTriggerScripted = technology.flags.HasFlag(RecipeFlags.HasResearchTriggerScripted);

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

if (isResearchTriggerCraft) {
BuildSubHeader(gui, LSs.TooltipHeaderTechnologyItemCrafting);
BuildSubHeader(gui, LSs.TooltipHeaderTechnologyCrafting);
using (gui.EnterGroup(contentPadding)) {
using var grid = gui.EnterInlineGrid(3f);
grid.Next();
_ = gui.BuildFactorioObjectWithAmount(technology.ingredients[0].goods, technology.ingredients[0].amount, ButtonDisplayStyle.ProductionTableUnscaled);
_ = gui.BuildFactorioObjectWithAmount(technology.ingredients[0].goods.With(technology.triggerMinimumQuality),
technology.ingredients[0].amount, ButtonDisplayStyle.ProductionTableUnscaled);
}
}
else if (isResearchTriggerCapture) {
Expand All @@ -625,7 +627,7 @@ private static void BuildTechnology(Technology technology, ImGui gui) {
else if (isResearchTriggerBuild) {
BuildSubHeader(gui, LSs.TooltipHeaderTechnologyBuildEntity.L(technology.triggerEntities.Count));
using (gui.EnterGroup(contentPadding)) {
BuildIconRow(gui, technology.triggerEntities, 2);
BuildIconRow(gui, [.. technology.triggerEntities.Select(e => e.With(technology.triggerMinimumQuality))], 2);
}
}
else if (isResearchTriggerPlatform) {
Expand All @@ -638,7 +640,14 @@ private static void BuildTechnology(Technology technology, ImGui gui) {
else if (isResearchTriggerLaunch) {
BuildSubHeader(gui, LSs.TooltipHeaderTechnologyLaunchItem.L(1));
using (gui.EnterGroup(contentPadding)) {
gui.BuildFactorioObjectButtonWithText(technology.triggerItem);
gui.BuildFactorioObjectButtonWithText(technology.triggerObject);
}
}
else if (isResearchTriggerScripted) {
BuildSubHeader(gui, LSs.TooltipHeaderTechnologyScripted);
using (gui.EnterGroup(contentPadding)) {
// null-forgiving: triggerObject is set before setting HasResearchTriggerScripted
gui.BuildText(technology.triggerObject!.locDescr, TextBlockDisplayStyle.WrappedText);
}
}

Expand Down
Loading