From 601b4a1368838fd5275a40ae26057fc1b7b5cd06 Mon Sep 17 00:00:00 2001 From: Dale McCoy <21223975+DaleStan@users.noreply.github.com> Date: Thu, 7 Nov 2024 00:44:45 -0500 Subject: [PATCH 1/6] feat: Add spoilage information to items in the gui. --- Yafc.Model/Data/DataClasses.cs | 29 +++++++++++++++++++ Yafc.Parser/Data/FactorioDataDeserializer.cs | 30 ++++++++++++++++++-- Yafc/Utils/ObjectDisplayStyles.cs | 3 +- Yafc/Widgets/ImmediateWidgets.cs | 10 +++++-- Yafc/Widgets/ObjectTooltip.cs | 12 +++++++- changelog.txt | 3 +- 6 files changed, 80 insertions(+), 7 deletions(-) diff --git a/Yafc.Model/Data/DataClasses.cs b/Yafc.Model/Data/DataClasses.cs index 6e376142..4c0a0197 100644 --- a/Yafc.Model/Data/DataClasses.cs +++ b/Yafc.Model/Data/DataClasses.cs @@ -329,6 +329,12 @@ public virtual bool HasSpentFuel([MaybeNullWhen(false)] out Item spent) { } public class Item : Goods { + public Item() { + getSpoilResult = new(() => getSpoilRecipe()?.products[0].goods); + getBaseSpoilTime = new(() => getSpoilRecipe()?.time ?? 0); + Recipe? getSpoilRecipe() => Database.recipes.all.OfType().SingleOrDefault(r => r.name == "spoil." + name); + } + /// /// The prototypes in this array will be loaded in order, before any other prototypes. /// This should correspond to the prototypes for subclasses of item, with more derived classes listed before their base classes. @@ -347,6 +353,29 @@ public class Item : Goods { public override string type => "Item"; internal override FactorioObjectSortOrder sortingOrder => FactorioObjectSortOrder.Items; public override UnitOfMeasure flowUnitOfMeasure => UnitOfMeasure.ItemPerSecond; + /// + /// Gets the result when this item spoils, or if this item doesn't spoil. + /// + public FactorioObject? spoilResult => getSpoilResult.Value; + /// + /// Gets the time it takes for a base-quality item to spoil, in seconds, or 0 if this item doesn't spoil. + /// + public float baseSpoilTime => getBaseSpoilTime.Value; + /// + /// Gets the -adjusted spoilage time for this item, in seconds, or 0 if this item doesn't spoil. + /// + public float GetSpoilTime(Quality quality) => quality.ApplyStandardBonus(baseSpoilTime); + + /// + /// The lazy store for getting the spoilage result. By default it searches for and reads a $"Mechanics.spoil.{name}" recipe, + /// but it can be overridden for items that spoil into Entities. + /// + internal Lazy getSpoilResult; + /// + /// The lazy store for getting the normal-quality spoilage time. By default it searches for and reads a $"Mechanics.spoil.{name}" recipe, + /// but it can be overridden for items that spoil into Entities. + /// + internal Lazy getBaseSpoilTime; public override bool HasSpentFuel([NotNullWhen(true)] out Item? spent) { spent = fuelResult; diff --git a/Yafc.Parser/Data/FactorioDataDeserializer.cs b/Yafc.Parser/Data/FactorioDataDeserializer.cs index 19d06cf1..23a8fe3b 100644 --- a/Yafc.Parser/Data/FactorioDataDeserializer.cs +++ b/Yafc.Parser/Data/FactorioDataDeserializer.cs @@ -374,7 +374,7 @@ private static EffectReceiver ParseEffectReceiver(LuaTable? table) { }; } - private void DeserializeItem(LuaTable table, ErrorCollector _) { + private void DeserializeItem(LuaTable table, ErrorCollector _1) { string name = table.Get("name", ""); if (table.Get("type", "") == "module" && table.Get("effect", out LuaTable? moduleEffect)) { Module module = GetObject(name); @@ -447,12 +447,38 @@ void readTrigger(LuaTable table) { recipe.time = 0f; // TODO what to put here? } - if (GetRef(table, "spoil_result", out Item? spoiled)) { + if (GetRef(table, "spoil_result", out Item? spoiled)) { var recipe = CreateSpecialRecipe(item, SpecialNames.SpoilRecipe, "spoiling"); recipe.ingredients = [new Ingredient(item, 1)]; recipe.products = [new Product(spoiled, 1)]; recipe.time = table.Get("spoil_ticks", 0) / 60f; } + // Read spoil-into-entity triggers + else if (table["spoil_to_trigger_result"] is LuaTable spoil && spoil["trigger"] is LuaTable triggers) { + triggers.ReadObjectOrArray(readTrigger); + + void readTrigger(LuaTable trigger) { + if (trigger.Get("type") == "direct" && trigger["action_delivery"] is LuaTable delivery) { + delivery.ReadObjectOrArray(readDelivery); + } + } + void readDelivery(LuaTable delivery) { + if (delivery.Get("type") == "instant" && delivery["source_effects"] is LuaTable effects) { + effects.ReadObjectOrArray(readEffect); + } + } + void readEffect(LuaTable effect) { + if (effect.Get("type") == "create-entity") { + float spoilTime = table.Get("spoil_ticks", 0) / 60f; + string entityName = "Entity." + effect.Get("entity_name"); + item.getBaseSpoilTime = new(() => spoilTime); + item.getSpoilResult = new(() => { + _ = Database.objectsByTypeName.TryGetValue(entityName, out FactorioObject? result); + return result; + }); + } + } + } if (table.Get("plant_result", out string? plantResult) && !string.IsNullOrEmpty(plantResult)) { plantResults[item] = plantResult; diff --git a/Yafc/Utils/ObjectDisplayStyles.cs b/Yafc/Utils/ObjectDisplayStyles.cs index d41ed9f7..022773e9 100644 --- a/Yafc/Utils/ObjectDisplayStyles.cs +++ b/Yafc/Utils/ObjectDisplayStyles.cs @@ -8,7 +8,8 @@ namespace Yafc.UI; /// The icon size. The production tables use size 3. /// The option to use when drawing the icon. /// Whether or not to obey the setting. -public record IconDisplayStyle(float Size, MilestoneDisplay MilestoneDisplay, bool UseScaleSetting) { +/// If , this icon will always be drawn as if the is accessible. +public record IconDisplayStyle(float Size, MilestoneDisplay MilestoneDisplay, bool UseScaleSetting, bool AlwaysAccessible = false) { /// /// Gets the default icon style: Size 2, , and not scaled. /// diff --git a/Yafc/Widgets/ImmediateWidgets.cs b/Yafc/Widgets/ImmediateWidgets.cs index acc5e612..f074287e 100644 --- a/Yafc/Widgets/ImmediateWidgets.cs +++ b/Yafc/Widgets/ImmediateWidgets.cs @@ -68,7 +68,7 @@ public static void BuildFactorioObjectIcon(this ImGui gui, IFactorioObjectWrappe return; } - SchemeColor color = obj.target.IsAccessible() ? SchemeColor.Source : SchemeColor.SourceFaint; + SchemeColor color = (obj.target.IsAccessible() || displayStyle.AlwaysAccessible) ? SchemeColor.Source : SchemeColor.SourceFaint; if (displayStyle.UseScaleSetting) { Rect rect = gui.AllocateRect(displayStyle.Size, displayStyle.Size, RectAlignment.Middle); gui.DrawIcon(rect.Expand(displayStyle.Size * (Project.current.preferences.iconScale - 1) / 2), obj.target.icon, color); @@ -96,6 +96,12 @@ public static void BuildFactorioObjectIcon(this ImGui gui, IFactorioObjectWrappe gui.DrawIcon(qualityRect, quality.icon, SchemeColor.Source); } + if (gui.isBuilding && obj.target is Item { baseSpoilTime: > 0 }) { + Vector2 size = new Vector2(displayStyle.Size / 2.5f); + Rect spoilableRect = new Rect(gui.lastRect.TopLeft, size); + + gui.DrawIcon(spoilableRect, Icon.Time, SchemeColor.PureForeground); + } } public static bool BuildFloatInput(this ImGui gui, DisplayAmount amount, TextBoxDisplayStyle displayStyle, SetKeyboardFocus setKeyboardFocus = SetKeyboardFocus.No) { @@ -163,7 +169,7 @@ public static Click BuildFactorioObjectButtonWithText(this ImGui gui, IFactorioO using (gui.EnterRow()) { gui.BuildFactorioObjectIcon(obj, iconDisplayStyle); var color = gui.textColor; - if (obj != null && !obj.target.IsAccessible()) { + if (obj != null && !obj.target.IsAccessible() && !iconDisplayStyle.AlwaysAccessible) { color += 1; } diff --git a/Yafc/Widgets/ObjectTooltip.cs b/Yafc/Widgets/ObjectTooltip.cs index 3ff9e3f9..a67ab9ea 100644 --- a/Yafc/Widgets/ObjectTooltip.cs +++ b/Yafc/Widgets/ObjectTooltip.cs @@ -333,6 +333,15 @@ private void BuildGoods(Goods goods, Quality quality, ImGui gui) { } } + if (goods is Item { spoilResult: FactorioObject spoiled } perishable) { + BuildSubHeader(gui, "Perishable"); + using (gui.EnterGroup(contentPadding)) { + float spoilTime = perishable.GetSpoilTime(quality); + gui.BuildText($"After {DataUtils.FormatTime(spoilTime)}, spoils into"); + gui.BuildFactorioObjectButtonWithText(spoiled, iconDisplayStyle: IconDisplayStyle.Default with { AlwaysAccessible = true }); + } + } + if (goods.fuelFor.Length > 0) { if (goods.fuelValue > 0f) { BuildSubHeader(gui, "Fuel value " + DataUtils.FormatAmount(goods.fuelValue, UnitOfMeasure.Megajoule) + " used for:"); @@ -588,7 +597,8 @@ private static void BuildQuality(Quality quality, ImGui gui) { ("Crafting speed:", '+' + DataUtils.FormatAmount(quality.StandardBonus, UnitOfMeasure.Percent)), ("Accumulator capacity:", '+' + DataUtils.FormatAmount(quality.AccumulatorCapacityBonus, UnitOfMeasure.Percent)), ("Module effects:", '+' + DataUtils.FormatAmount(quality.StandardBonus, UnitOfMeasure.Percent) + '*'), - ("Beacon transmission efficiency:", '+' + DataUtils.FormatAmount(quality.BeaconTransmissionBonus, UnitOfMeasure.None)) + ("Beacon transmission efficiency:", '+' + DataUtils.FormatAmount(quality.BeaconTransmissionBonus, UnitOfMeasure.None)), + ("Time before spoiling:", '+' + DataUtils.FormatAmount(quality.StandardBonus, UnitOfMeasure.Percent)), ]; float rightWidth = text.Max(t => gui.GetTextDimensions(out _, t.right).X); diff --git a/changelog.txt b/changelog.txt index 2acadf37..d539e349 100644 --- a/changelog.txt +++ b/changelog.txt @@ -18,11 +18,12 @@ Version: Date: Features: - - (SA) Process accessiblilty of captured spawners, which also fixes biter eggs and subsequent Gleba recipes. + - (SA) Process accessibilty of captured spawners, which also fixes biter eggs and subsequent Gleba recipes. - (SA) Add support for the capture-spawner technology trigger. - Add the remaining research triggers and the mining-with-fluid research effect to the dependency/milestone analysis. - Explain what to do if Yafc fails to load a mod. + - (SA) Add spoiling time and result to item tooltips. Add a clock overlay to icons for perishable items. Internal changes: - Using the LuaContext after it is freed now produces a better error. ---------------------------------------------------------------------------------------------------------------------- From 3570c6c86c5d2f48526eee64a9509cae3282c7fb Mon Sep 17 00:00:00 2001 From: Dale McCoy <21223975+DaleStan@users.noreply.github.com> Date: Fri, 8 Nov 2024 15:08:09 -0500 Subject: [PATCH 2/6] feat: Add the spoiling rate preference --- Yafc.Model/Model/Project.cs | 2 ++ Yafc/Widgets/ObjectTooltip.cs | 2 +- Yafc/Windows/PreferencesScreen.cs | 9 +++++++++ 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/Yafc.Model/Model/Project.cs b/Yafc.Model/Model/Project.cs index 43bb8893..50c5bc35 100644 --- a/Yafc.Model/Model/Project.cs +++ b/Yafc.Model/Model/Project.cs @@ -225,6 +225,8 @@ public class ProjectSettings(Project project) : ModelObject(project) { public int reactorSizeX { get; set; } = 2; public int reactorSizeY { get; set; } = 2; public float PollutionCostModifier { get; set; } = 0; + public float spoilingRate { get; set; } = 1; + public event Action? changed; protected internal override void ThisChanged(bool visualOnly) => changed?.Invoke(visualOnly); diff --git a/Yafc/Widgets/ObjectTooltip.cs b/Yafc/Widgets/ObjectTooltip.cs index a67ab9ea..c93b9c9b 100644 --- a/Yafc/Widgets/ObjectTooltip.cs +++ b/Yafc/Widgets/ObjectTooltip.cs @@ -336,7 +336,7 @@ private void BuildGoods(Goods goods, Quality quality, ImGui gui) { if (goods is Item { spoilResult: FactorioObject spoiled } perishable) { BuildSubHeader(gui, "Perishable"); using (gui.EnterGroup(contentPadding)) { - float spoilTime = perishable.GetSpoilTime(quality); + float spoilTime = perishable.GetSpoilTime(quality) / Project.current.settings.spoilingRate; gui.BuildText($"After {DataUtils.FormatTime(spoilTime)}, spoils into"); gui.BuildFactorioObjectButtonWithText(spoiled, iconDisplayStyle: IconDisplayStyle.Default with { AlwaysAccessible = true }); } diff --git a/Yafc/Windows/PreferencesScreen.cs b/Yafc/Windows/PreferencesScreen.cs index b828312d..8f0ef6db 100644 --- a/Yafc/Windows/PreferencesScreen.cs +++ b/Yafc/Windows/PreferencesScreen.cs @@ -157,6 +157,15 @@ private static void DrawGeneral(ImGui gui) { } } + using (gui.EnterRowWithHelpIcon("Set this to match the spoiling rate you selected when starting your game. 10% is slow spoiling, and 1000% (1k%) is fast spoiling.")) { + gui.BuildText("Spoiling rate:", topOffset: 0.5f); + DisplayAmount amount = new(settings.spoilingRate, UnitOfMeasure.Percent); + if (gui.BuildFloatInput(amount, TextBoxDisplayStyle.DefaultTextInput)) { + settings.RecordUndo().spoilingRate = Math.Clamp(amount.Value, .1f, 10); + gui.Rebuild(); + } + } + gui.AllocateSpacing(); if (gui.BuildCheckBox("Dark mode", Preferences.Instance.darkMode, out bool newValue)) { From 4c2538dfaeafe69eccf39c07f963ab4b2f125c8c Mon Sep 17 00:00:00 2001 From: Dale McCoy <21223975+DaleStan@users.noreply.github.com> Date: Sat, 9 Nov 2024 00:25:58 -0500 Subject: [PATCH 3/6] feat: Load and display fixed spoilage recipe output information. This is RecipePrototype.result_is_always_fresh and ItemProductPrototype.percent_spoiled. --- Yafc.Model/Data/DataClasses.cs | 20 ++++++++++++++++++- Yafc.Model/Model/ProductionTableContent.cs | 14 +++++++------ Yafc.Parser/Data/DataParserUtils.cs | 18 +++++++++++------ ...rioDataDeserializer_RecipeAndTechnology.cs | 12 +++++++---- Yafc/Widgets/ObjectTooltip.cs | 9 +++++++-- .../ProductionTable/ProductionTableView.cs | 16 +++++++++++++-- 6 files changed, 68 insertions(+), 21 deletions(-) diff --git a/Yafc.Model/Data/DataClasses.cs b/Yafc.Model/Data/DataClasses.cs index 4c0a0197..cfdff620 100644 --- a/Yafc.Model/Data/DataClasses.cs +++ b/Yafc.Model/Data/DataClasses.cs @@ -248,6 +248,11 @@ public class Product : IFactorioObjectWrapper { internal readonly float amountMax; internal readonly float probability; public readonly float amount; // This is average amount including probability and range + /// + /// Gets or sets the fixed freshness of this product: 0 if the recipe has result_is_always_fresh, or percent_spoiled from the + /// ItemProductPrototype, or if neither of those values are set. + /// + public float? percentSpoiled { get; internal set; } internal float productivityAmount { get; private set; } public void SetCatalyst(float catalyst) { @@ -283,7 +288,10 @@ public Product(Goods goods, float min, float max, float probability) { amount = productivityAmount = probability * (min + max) / 2; } - public bool IsSimple => amountMin == amountMax && probability == 1f; + /// + /// Gets if this product is one item with 100% probability and default spoilage behavior. + /// + public bool IsSimple => amountMin == amountMax && amount == 1 && probability == 1 && percentSpoiled == null; FactorioObject IFactorioObjectWrapper.target => goods; @@ -301,6 +309,16 @@ string IFactorioObjectWrapper.text { if (probability != 1f) { text = DataUtils.FormatAmount(probability, UnitOfMeasure.Percent) + " " + text; } + else if (amountMin == 1 && amountMax == 1) { + text = "1x " + text; + } + + if (percentSpoiled == 0) { + text += ", always fresh"; + } + else if (percentSpoiled != null) { + text += ", " + DataUtils.FormatAmount(percentSpoiled.Value, UnitOfMeasure.Percent) + " spoiled"; + } return text; } diff --git a/Yafc.Model/Model/ProductionTableContent.cs b/Yafc.Model/Model/ProductionTableContent.cs index baa443cf..f7c026e8 100644 --- a/Yafc.Model/Model/ProductionTableContent.cs +++ b/Yafc.Model/Model/ProductionTableContent.cs @@ -449,15 +449,15 @@ public IEnumerable Products { handledFuel = true; } - yield return (product.goods, amount, links.products[i++]); + yield return (product.goods, amount, links.products[i++], product.percentSpoiled); } else { - yield return (null, 0, null); + yield return (null, 0, null, null); } } if (!handledFuel) { - yield return hierarchyEnabled ? (spentFuel, fuelUsagePerSecond, links.spentFuel) : (null, 0, null); + yield return hierarchyEnabled ? (spentFuel, fuelUsagePerSecond, links.spentFuel, null) : (null, 0, null, null); } } } @@ -745,7 +745,9 @@ public static implicit operator RecipeRowIngredient((Goods? Goods, float Amount, => new(value.Goods, value.Amount, value.Link, value.Variants); } -public record RecipeRowProduct(Goods? Goods, float Amount, ProductionLink? Link) { - public static implicit operator (Goods? Goods, float Amount, ProductionLink? Link)(RecipeRowProduct value) => (value.Goods, value.Amount, value.Link); - public static implicit operator RecipeRowProduct((Goods? Goods, float Amount, ProductionLink? Link) value) => new(value.Goods, value.Amount, value.Link); +public record RecipeRowProduct(Goods? Goods, float Amount, ProductionLink? Link, float? PercentSpoiled) { + public static implicit operator (Goods? Goods, float Amount, ProductionLink? Link, float? PercentSpoiled)(RecipeRowProduct value) + => (value.Goods, value.Amount, value.Link, value.PercentSpoiled); + public static implicit operator RecipeRowProduct((Goods? Goods, float Amount, ProductionLink? Link, float? PercentSpoiled) value) + => new(value.Goods, value.Amount, value.Link, value.PercentSpoiled); } diff --git a/Yafc.Parser/Data/DataParserUtils.cs b/Yafc.Parser/Data/DataParserUtils.cs index 41d3a638..6706523e 100644 --- a/Yafc.Parser/Data/DataParserUtils.cs +++ b/Yafc.Parser/Data/DataParserUtils.cs @@ -7,27 +7,33 @@ namespace Yafc.Parser; internal static class DataParserUtils { private static class ConvertersFromLua { - public static Func? convert; + public static Converter? convert; + + [return: NotNullIfNotNull(nameof(@default))] + public delegate T Converter(object value, T @default); } static DataParserUtils() { ConvertersFromLua.convert = (o, def) => o is long l ? (int)l : o is double d ? (int)d : o is string s && int.TryParse(s, out int res) ? res : def; + ConvertersFromLua.convert = (o, def) => o is long l ? (int)l : o is double d ? (int)d : o is string s && int.TryParse(s, out int res) ? res : def; ConvertersFromLua.convert = (o, def) => o is long l ? l : o is double d ? (float)d : o is string s && float.TryParse(s, out float res) ? res : def; - ConvertersFromLua.convert = delegate (object src, bool def) { + ConvertersFromLua.convert = (o, def) => o is long l ? l : o is double d ? (float)d : o is string s && float.TryParse(s, out float res) ? res : def; + ConvertersFromLua.convert = (o, def) => ConvertersFromLua.convert!(o, def).Value; + ConvertersFromLua.convert = (o, def) => { - if (src is bool b) { + if (o is bool b) { return b; } - if (src == null) { + if (o == null) { return def; } - if (src.Equals("true")) { + if (o.Equals("true")) { return true; } - if (src.Equals("false")) { + if (o.Equals("false")) { return false; } diff --git a/Yafc.Parser/Data/FactorioDataDeserializer_RecipeAndTechnology.cs b/Yafc.Parser/Data/FactorioDataDeserializer_RecipeAndTechnology.cs index b1984272..f9b6efab 100644 --- a/Yafc.Parser/Data/FactorioDataDeserializer_RecipeAndTechnology.cs +++ b/Yafc.Parser/Data/FactorioDataDeserializer_RecipeAndTechnology.cs @@ -168,7 +168,7 @@ private void LoadTechnologyData(Technology technology, LuaTable table, ErrorColl } } - private Func LoadProduct(string typeDotName, int multiplier = 1) => table => { + private Func LoadProduct(string typeDotName, int multiplier = 1, float? percentSpoiled = null) => table => { Goods? goods = LoadItemOrFluid(table, true); float min, max, catalyst; @@ -184,6 +184,8 @@ private Func LoadProduct(string typeDotName, int multiplier = throw new NotSupportedException($"Could not load amount for one of the products for {typeDotName}, possibly named '{table.Get("name", "")}'."); } + percentSpoiled ??= table.Get("percent_spoiled"); + float extraCountFraction = table.Get("extra_count_fraction", 0f); min += extraCountFraction; max += extraCountFraction; @@ -207,7 +209,7 @@ private Func LoadProduct(string typeDotName, int multiplier = throw new NotSupportedException($"Could not load one of the products for {typeDotName}, possibly named '{table.Get("name", "")}'."); } - Product product = new Product(goods, min * multiplier, max * multiplier, table.Get("probability", 1f)); + Product product = new Product(goods, min * multiplier, max * multiplier, table.Get("probability", 1f)) { percentSpoiled = percentSpoiled }; if (catalyst > 0f) { product.SetCatalyst(catalyst); @@ -217,12 +219,14 @@ private Func LoadProduct(string typeDotName, int multiplier = }; private Product[] LoadProductList(LuaTable table, string typeDotName, bool allowSimpleSyntax) { + float? percentSpoiled = table.Get("result_is_always_fresh", false) ? 0 : null; + if (table.Get("results", out LuaTable? resultList)) { - return resultList.ArrayElements().Select(LoadProduct(typeDotName)).Where(x => x.amount != 0).ToArray(); + return resultList.ArrayElements().Select(LoadProduct(typeDotName, percentSpoiled: percentSpoiled)).Where(x => x.amount != 0).ToArray(); } if (allowSimpleSyntax && table.Get("result", out string? name)) { - return [(new Product(GetObject(name), 1))]; + return [(new Product(GetObject(name), 1) { percentSpoiled = percentSpoiled })]; } return []; diff --git a/Yafc/Widgets/ObjectTooltip.cs b/Yafc/Widgets/ObjectTooltip.cs index c93b9c9b..7f483368 100644 --- a/Yafc/Widgets/ObjectTooltip.cs +++ b/Yafc/Widgets/ObjectTooltip.cs @@ -339,6 +339,7 @@ private void BuildGoods(Goods goods, Quality quality, ImGui gui) { float spoilTime = perishable.GetSpoilTime(quality) / Project.current.settings.spoilingRate; gui.BuildText($"After {DataUtils.FormatTime(spoilTime)}, spoils into"); gui.BuildFactorioObjectButtonWithText(spoiled, iconDisplayStyle: IconDisplayStyle.Default with { AlwaysAccessible = true }); + tooltipOptions.ExtraSpoilInformation?.Invoke(gui); } } @@ -441,7 +442,7 @@ private static void BuildRecipe(RecipeOrTechnology recipe, ImGui gui) { } } - if (recipe.products.Length > 0 && !(recipe.products.Length == 1 && recipe.products[0].IsSimple && recipe.products[0].goods is Item && recipe.products[0].amount == 1f)) { + if (recipe.products.Length > 0 && !(recipe.products.Length == 1 && recipe.products[0].IsSimple && recipe.products[0].goods is Item)) { BuildSubHeader(gui, "Products"); using (gui.EnterGroup(contentPadding)) { foreach (var product in recipe.products) { @@ -634,9 +635,13 @@ public struct ObjectTooltipOptions { /// public HintLocations HintLocations { get; set; } /// - /// Gets or sets a value that, if not null, will be called after drawing the tooltip header. + /// Gets or sets a value that, if not , will be called after drawing the tooltip header. /// public DrawBelowHeader? DrawBelowHeader { get; set; } + /// + /// Gets or sets a value that, if not , will be called after drawing the spoilage information. + /// + public GuiBuilder ExtraSpoilInformation { get; set; } // Reduce boilerplate by permitting unambiguous and relatively obvious implicit conversions. public static implicit operator ObjectTooltipOptions(HintLocations hintLocations) => new() { HintLocations = hintLocations }; diff --git a/Yafc/Workspace/ProductionTable/ProductionTableView.cs b/Yafc/Workspace/ProductionTable/ProductionTableView.cs index 84801f84..b0b82546 100644 --- a/Yafc/Workspace/ProductionTable/ProductionTableView.cs +++ b/Yafc/Workspace/ProductionTable/ProductionTableView.cs @@ -584,9 +584,21 @@ public override void BuildElement(ImGui gui, RecipeRow recipe) { view.BuildTableProducts(gui, recipe.subgroup, recipe.owner, ref grid, false); } else { - foreach (var (goods, amount, link) in recipe.Products) { + foreach (var (goods, amount, link, percentSpoiled) in recipe.Products) { grid.Next(); - view.BuildGoodsIcon(gui, goods, link, amount, ProductDropdownType.Product, recipe, recipe.linkRoot, HintLocations.OnConsumingRecipes); + if (percentSpoiled == null) { + view.BuildGoodsIcon(gui, goods, link, amount, ProductDropdownType.Product, recipe, recipe.linkRoot, HintLocations.OnConsumingRecipes); + } + else if (percentSpoiled == 0) { + view.BuildGoodsIcon(gui, goods, link, amount, ProductDropdownType.Product, recipe, recipe.linkRoot, + new() { HintLocations = HintLocations.OnConsumingRecipes, ExtraSpoilInformation = gui => gui.BuildText("This recipe output is always fresh.") }); + } + else { + view.BuildGoodsIcon(gui, goods, link, amount, ProductDropdownType.Product, recipe, recipe.linkRoot, new() { + HintLocations = HintLocations.OnConsumingRecipes, + ExtraSpoilInformation = gui => gui.BuildText($"This recipe output is {DataUtils.FormatAmount(percentSpoiled.Value, UnitOfMeasure.Percent)} spoiled.") + }); + } } if (recipe.fixedProduct == Database.itemOutput || recipe.showTotalIO) { grid.Next(); From ace073ccc3895dd1984ac8e07304f5f08070c50a Mon Sep 17 00:00:00 2001 From: Dale McCoy <21223975+DaleStan@users.noreply.github.com> Date: Sat, 9 Nov 2024 01:04:13 -0500 Subject: [PATCH 4/6] feat: Load and display preserve_products_in_machine_output. --- Yafc.Model/Data/DataClasses.cs | 2 ++ .../Data/FactorioDataDeserializer_RecipeAndTechnology.cs | 1 + Yafc/Widgets/ObjectTooltip.cs | 7 ++++--- Yafc/Workspace/ProductionTable/ProductionTableView.cs | 8 +++++++- 4 files changed, 14 insertions(+), 4 deletions(-) diff --git a/Yafc.Model/Data/DataClasses.cs b/Yafc.Model/Data/DataClasses.cs index cfdff620..45efd4f5 100644 --- a/Yafc.Model/Data/DataClasses.cs +++ b/Yafc.Model/Data/DataClasses.cs @@ -170,6 +170,8 @@ public class Recipe : RecipeOrTechnology { public string[]? allowedModuleCategories { get; internal set; } public Technology[] technologyUnlock { get; internal set; } = []; public Dictionary technologyProductivity { get; internal set; } = []; + public bool preserveProducts { get; internal set; } + public bool HasIngredientVariants() { foreach (var ingredient in ingredients) { if (ingredient.variants != null) { diff --git a/Yafc.Parser/Data/FactorioDataDeserializer_RecipeAndTechnology.cs b/Yafc.Parser/Data/FactorioDataDeserializer_RecipeAndTechnology.cs index f9b6efab..d327cab3 100644 --- a/Yafc.Parser/Data/FactorioDataDeserializer_RecipeAndTechnology.cs +++ b/Yafc.Parser/Data/FactorioDataDeserializer_RecipeAndTechnology.cs @@ -336,6 +336,7 @@ private void LoadRecipeData(Recipe recipe, LuaTable table, ErrorCollector errorC recipe.products = LoadProductList(table, recipe.typeDotName, allowSimpleSyntax: false); recipe.time = table.Get("energy_required", 0.5f); + recipe.preserveProducts = table.Get("preserve_products_in_machine_output", false); if (table.Get("main_product", out string? mainProductName) && mainProductName != "") { recipe.mainProduct = recipe.products.FirstOrDefault(x => x.goods.name == mainProductName)?.goods; diff --git a/Yafc/Widgets/ObjectTooltip.cs b/Yafc/Widgets/ObjectTooltip.cs index 7f483368..b44633f0 100644 --- a/Yafc/Widgets/ObjectTooltip.cs +++ b/Yafc/Widgets/ObjectTooltip.cs @@ -117,10 +117,10 @@ private static void BuildIconRow(ImGui gui, IReadOnlyList object } } - private static void BuildItem(ImGui gui, IFactorioObjectWrapper item) { + private static void BuildItem(ImGui gui, IFactorioObjectWrapper item, string? extraText = null) { using (gui.EnterRow()) { gui.BuildFactorioObjectIcon(item.target); - gui.BuildText(item.text, TextBlockDisplayStyle.WrappedText); + gui.BuildText(item.text + extraText, TextBlockDisplayStyle.WrappedText); } } @@ -445,8 +445,9 @@ private static void BuildRecipe(RecipeOrTechnology recipe, ImGui gui) { if (recipe.products.Length > 0 && !(recipe.products.Length == 1 && recipe.products[0].IsSimple && recipe.products[0].goods is Item)) { BuildSubHeader(gui, "Products"); using (gui.EnterGroup(contentPadding)) { + string? extraText = recipe is Recipe { preserveProducts: true } ? ", preserved until removed from the machine" : null; foreach (var product in recipe.products) { - BuildItem(gui, product); + BuildItem(gui, product, extraText); } } } diff --git a/Yafc/Workspace/ProductionTable/ProductionTableView.cs b/Yafc/Workspace/ProductionTable/ProductionTableView.cs index b0b82546..c5985d4f 100644 --- a/Yafc/Workspace/ProductionTable/ProductionTableView.cs +++ b/Yafc/Workspace/ProductionTable/ProductionTableView.cs @@ -586,7 +586,13 @@ public override void BuildElement(ImGui gui, RecipeRow recipe) { else { foreach (var (goods, amount, link, percentSpoiled) in recipe.Products) { grid.Next(); - if (percentSpoiled == null) { + if (recipe.recipe is Recipe { preserveProducts: true }) { + view.BuildGoodsIcon(gui, goods, link, amount, ProductDropdownType.Product, recipe, recipe.linkRoot, new() { + HintLocations = HintLocations.OnConsumingRecipes, + ExtraSpoilInformation = gui => gui.BuildText("This recipe output does not start spoiling until removed from the machine.", TextBlockDisplayStyle.WrappedText) + }); + } + else if (percentSpoiled == null) { view.BuildGoodsIcon(gui, goods, link, amount, ProductDropdownType.Product, recipe, recipe.linkRoot, HintLocations.OnConsumingRecipes); } else if (percentSpoiled == 0) { From c18661f5ad49b9d019ff7dd5b4683b5619e4bc42 Mon Sep 17 00:00:00 2001 From: Dale McCoy <21223975+DaleStan@users.noreply.github.com> Date: Sat, 9 Nov 2024 01:24:55 -0500 Subject: [PATCH 5/6] chore: Alphabetize cases for entity types. --- .../Data/FactorioDataDeserializer_Entity.cs | 245 +++++++++--------- 1 file changed, 119 insertions(+), 126 deletions(-) diff --git a/Yafc.Parser/Data/FactorioDataDeserializer_Entity.cs b/Yafc.Parser/Data/FactorioDataDeserializer_Entity.cs index 36c9d24d..c5b28440 100644 --- a/Yafc.Parser/Data/FactorioDataDeserializer_Entity.cs +++ b/Yafc.Parser/Data/FactorioDataDeserializer_Entity.cs @@ -169,33 +169,30 @@ private void DeserializeEntity(LuaTable table, ErrorCollector errorCollector) { } switch (factorioType) { - case "transport-belt": - GetObject(name).beltItemsPerSecond = table.Get("speed", 0f) * 480f; - - break; - case "inserter": - var inserter = GetObject(name); - inserter.inserterSwingTime = 1f / (table.Get("rotation_speed", 1f) * 60); - inserter.isBulkInserter = table.Get("bulk", false); - - break; + // NOTE: Please add new cases in alphabetical order, even if the new case shares code with another case. That is, + // case "rocket-silo": + // goto case "furnace"; + // instead of + // case "furnace": + // case "rocket-silo": // Out of order case "accumulator": var accumulator = GetObject(name); if (table.Get("energy_source", out LuaTable? accumulatorEnergy) && accumulatorEnergy.Get("buffer_capacity", out string? capacity)) { accumulator.baseAccumulatorCapacity = ParseEnergy(capacity); } - break; - case "reactor": - var reactor = GetObject(name); - reactor.reactorNeighborBonus = table.Get("neighbour_bonus", 1f); // Keep UK spelling for Factorio/LUA data objects - _ = table.Get("consumption", out usesPower); - reactor.basePower = ParseEnergy(usesPower); - reactor.baseCraftingSpeed = reactor.basePower; - recipeCrafters.Add(reactor, SpecialNames.ReactorRecipe); - + case "agricultural-tower": + var agriculturalTower = GetObject(name); + _ = table.Get("energy_usage", out usesPower); + agriculturalTower.basePower = ParseEnergy(usesPower); + float radius = table.Get("radius", 1f); + agriculturalTower.baseCraftingSpeed = (float)(Math.Pow(2 * radius + 1, 2) - 1); + agriculturalTower.itemInputs = 1; + recipeCrafters.Add(agriculturalTower, SpecialNames.PlantRecipe); break; + case "assembling-machine": + goto case "furnace"; case "beacon": var beacon = GetObject(name); beacon.baseBeaconEfficiency = table.Get("distribution_effectivity", 0f); @@ -203,21 +200,51 @@ private void DeserializeEntity(LuaTable table, ErrorCollector errorCollector) { _ = table.Get("energy_usage", out usesPower); ParseModules(table, beacon, AllowedEffects.None); beacon.basePower = ParseEnergy(usesPower); + break; + case "boiler": + var boiler = GetObject(name); + _ = table.Get("energy_consumption", out usesPower); + boiler.basePower = ParseEnergy(usesPower); + boiler.fluidInputs = 1; + bool hasOutput = table.Get("mode", out string? mode) && mode == "output-to-separate-pipe"; + _ = GetFluidBoxFilter(table, "fluid_box", 0, out Fluid? input, out var acceptTemperature); + _ = table.Get("target_temperature", out int targetTemp); + Fluid? output = hasOutput ? GetFluidBoxFilter(table, "output_fluid_box", targetTemp, out var fluid, out _) ? fluid : null : input; + if (input == null || output == null) { // TODO - boiler works with any fluid - not supported + break; + } + + // otherwise convert boiler production to a recipe + string category = SpecialNames.BoilerRecipe + boiler.name; + var recipe = CreateSpecialRecipe(output, category, "boiling to " + targetTemp + "°"); + recipeCrafters.Add(boiler, category); + recipe.flags |= RecipeFlags.UsesFluidTemperature; + // TODO: input fluid amount now depends on its temperature, using min temperature should be OK for non-modded + float inputEnergyPerOneFluid = (targetTemp - acceptTemperature.min) * input.heatCapacity; + recipe.ingredients = [new Ingredient(input, boiler.basePower / inputEnergyPerOneFluid) { temperature = acceptTemperature }]; + float outputEnergyPerOneFluid = (targetTemp - output.temperatureRange.min) * output.heatCapacity; + recipe.products = [new Product(output, boiler.basePower / outputEnergyPerOneFluid)]; + recipe.time = 1f; + boiler.baseCraftingSpeed = 1f; break; - case "logistic-container": - case "container": - var container = GetObject(name); - container.inventorySize = table.Get("inventory_size", 0); + case "burner-generator": + var generator = GetObject(name); - if (factorioType == "logistic-container") { - container.logisticMode = table.Get("logistic_mode", ""); - container.logisticSlotsCount = table.Get("logistic_slots_count", 0); - if (container.logisticSlotsCount == 0) { - container.logisticSlotsCount = table.Get("max_logistic_slots", 1000); - } + // generator energy input config is strange + if (table.Get("max_power_output", out string? maxPowerOutput)) { + generator.basePower = ParseEnergy(maxPowerOutput); + } + + if ((factorioVersion < v0_18 || factorioType == "burner-generator") && table.Get("burner", out LuaTable? burnerSource)) { + ReadEnergySource(burnerSource, generator); + } + else { + generator.energy = new EntityEnergy { effectivity = table.Get("effectivity", 1f) }; + ReadFluidEnergySource(table, generator); } + recipeCrafters.Add(generator, SpecialNames.GeneratorRecipe); break; case "character": var character = GetObject(name); @@ -244,38 +271,35 @@ private void DeserializeEntity(LuaTable table, ErrorCollector errorCollector) { character.mapGenerated = true; rootAccessible.Insert(0, character); } - break; - case "boiler": - var boiler = GetObject(name); - _ = table.Get("energy_consumption", out usesPower); - boiler.basePower = ParseEnergy(usesPower); - boiler.fluidInputs = 1; - bool hasOutput = table.Get("mode", out string? mode) && mode == "output-to-separate-pipe"; - _ = GetFluidBoxFilter(table, "fluid_box", 0, out Fluid? input, out var acceptTemperature); - _ = table.Get("target_temperature", out int targetTemp); - Fluid? output = hasOutput ? GetFluidBoxFilter(table, "output_fluid_box", targetTemp, out var fluid, out _) ? fluid : null : input; - - if (input == null || output == null) { // TODO - boiler works with any fluid - not supported - break; + case "constant-combinator": + if (name == "constant-combinator") { + Database.constantCombinatorCapacity = table.Get("item_slot_count", 18); } + break; + case "container": + var container = GetObject(name); + container.inventorySize = table.Get("inventory_size", 0); - // otherwise convert boiler production to a recipe - string category = SpecialNames.BoilerRecipe + boiler.name; - var recipe = CreateSpecialRecipe(output, category, "boiling to " + targetTemp + "°"); - recipeCrafters.Add(boiler, category); - recipe.flags |= RecipeFlags.UsesFluidTemperature; - // TODO: input fluid amount now depends on its temperature, using min temperature should be OK for non-modded - float inputEnergyPerOneFluid = (targetTemp - acceptTemperature.min) * input.heatCapacity; - recipe.ingredients = [new Ingredient(input, boiler.basePower / inputEnergyPerOneFluid) { temperature = acceptTemperature }]; - float outputEnergyPerOneFluid = (targetTemp - output.temperatureRange.min) * output.heatCapacity; - recipe.products = [new Product(output, boiler.basePower / outputEnergyPerOneFluid)]; - recipe.time = 1f; - boiler.baseCraftingSpeed = 1f; + if (factorioType == "logistic-container") { + container.logisticMode = table.Get("logistic_mode", ""); + container.logisticSlotsCount = table.Get("logistic_slots_count", 0); + if (container.logisticSlotsCount == 0) { + container.logisticSlotsCount = table.Get("max_logistic_slots", 1000); + } + } + break; + case "electric-energy-interface": + var eei = GetObject(name); + eei.energy = voidEntityEnergy; + if (table.Get("energy_production", out string? interfaceProduction)) { + eei.baseCraftingSpeed = ParseEnergy(interfaceProduction); + if (eei.baseCraftingSpeed > 0) { + recipeCrafters.Add(eei, SpecialNames.GeneratorRecipe); + } + } break; - case "assembling-machine": - case "rocket-silo": case "furnace": var crafter = GetObject(name); _ = table.Get("energy_usage", out usesPower); @@ -330,28 +354,28 @@ private void DeserializeEntity(LuaTable table, ErrorCollector errorCollector) { } } } - break; case "generator": - case "burner-generator": - var generator = GetObject(name); - - // generator energy input config is strange - if (table.Get("max_power_output", out string? maxPowerOutput)) { - generator.basePower = ParseEnergy(maxPowerOutput); - } - - if ((factorioVersion < v0_18 || factorioType == "burner-generator") && table.Get("burner", out LuaTable? burnerSource)) { - ReadEnergySource(burnerSource, generator); - } - else { - generator.energy = new EntityEnergy { effectivity = table.Get("effectivity", 1f) }; - ReadFluidEnergySource(table, generator); - } - - recipeCrafters.Add(generator, SpecialNames.GeneratorRecipe); - + goto case "burner-generator"; + case "inserter": + var inserter = GetObject(name); + inserter.inserterSwingTime = 1f / (table.Get("rotation_speed", 1f) * 60); + inserter.isBulkInserter = table.Get("bulk", false); break; + case "lab": + var lab = GetObject(name); + _ = table.Get("energy_usage", out usesPower); + ParseModules(table, lab, AllowedEffects.All ^ AllowedEffects.Quality); + lab.basePower = ParseEnergy(usesPower); + lab.baseCraftingSpeed = table.Get("researching_speed", 1f); + recipeCrafters.Add(lab, SpecialNames.Labs); + _ = table.Get("inputs", out LuaTable? inputs); + lab.inputs = inputs.ArrayElements().Select(GetObject).ToArray(); + sciencePacks.UnionWith(lab.inputs.Select(x => (Item)x)); + lab.itemInputs = lab.inputs.Length; + break; + case "logistic-container": + goto case "container"; case "mining-drill": var drill = GetObject(name); _ = table.Get("energy_usage", out usesPower); @@ -370,16 +394,6 @@ private void DeserializeEntity(LuaTable table, ErrorCollector errorCollector) { foreach (string resource in resourceCategories.ArrayElements()) { recipeCrafters.Add(drill, SpecialNames.MiningRecipe + resource); } - - break; - case "agricultural-tower": - var agriculturalTower = GetObject(name); - _ = table.Get("energy_usage", out usesPower); - agriculturalTower.basePower = ParseEnergy(usesPower); - float radius = table.Get("radius", 1f); - agriculturalTower.baseCraftingSpeed = (float)(Math.Pow(2 * radius + 1, 2) - 1); - agriculturalTower.itemInputs = 1; - recipeCrafters.Add(agriculturalTower, SpecialNames.PlantRecipe); break; case "offshore-pump": var pump = GetObject(name); @@ -405,46 +419,6 @@ private void DeserializeEntity(LuaTable table, ErrorCollector errorCollector) { recipeCrafters.Add(pump, recipeCategory); pump.energy = voidEntityEnergy; } - - break; - case "lab": - var lab = GetObject(name); - _ = table.Get("energy_usage", out usesPower); - ParseModules(table, lab, AllowedEffects.All ^ AllowedEffects.Quality); - lab.basePower = ParseEnergy(usesPower); - lab.baseCraftingSpeed = table.Get("researching_speed", 1f); - recipeCrafters.Add(lab, SpecialNames.Labs); - _ = table.Get("inputs", out LuaTable? inputs); - lab.inputs = inputs.ArrayElements().Select(GetObject).ToArray(); - sciencePacks.UnionWith(lab.inputs.Select(x => (Item)x)); - lab.itemInputs = lab.inputs.Length; - - break; - case "solar-panel": - var solarPanel = GetObject(name); - solarPanel.energy = voidEntityEnergy; - _ = table.Get("production", out string? powerProduction); - recipeCrafters.Add(solarPanel, SpecialNames.GeneratorRecipe); - solarPanel.baseCraftingSpeed = ParseEnergy(powerProduction) * 0.7f; // 0.7f is a solar panel ratio on nauvis - - break; - case "electric-energy-interface": - var eei = GetObject(name); - eei.energy = voidEntityEnergy; - - if (table.Get("energy_production", out string? interfaceProduction)) { - eei.baseCraftingSpeed = ParseEnergy(interfaceProduction); - if (eei.baseCraftingSpeed > 0) { - recipeCrafters.Add(eei, SpecialNames.GeneratorRecipe); - } - } - - break; - case "constant-combinator": - if (name == "constant-combinator") { - Database.constantCombinatorCapacity = table.Get("item_slot_count", 18); - } - break; case "projectile": var projectile = GetObject(name); @@ -467,7 +441,26 @@ void parseEffect(LuaTable effect) { projectile.placeEntities.Add(createdEntity); } } - + break; + case "reactor": + var reactor = GetObject(name); + reactor.reactorNeighborBonus = table.Get("neighbour_bonus", 1f); // Keep UK spelling for Factorio/LUA data objects + _ = table.Get("consumption", out usesPower); + reactor.basePower = ParseEnergy(usesPower); + reactor.baseCraftingSpeed = reactor.basePower; + recipeCrafters.Add(reactor, SpecialNames.ReactorRecipe); + break; + case "rocket-silo": + goto case "furnace"; + case "solar-panel": + var solarPanel = GetObject(name); + solarPanel.energy = voidEntityEnergy; + _ = table.Get("production", out string? powerProduction); + recipeCrafters.Add(solarPanel, SpecialNames.GeneratorRecipe); + solarPanel.baseCraftingSpeed = ParseEnergy(powerProduction) * 0.7f; // 0.7f is a solar panel ratio on nauvis + break; + case "transport-belt": + GetObject(name).beltItemsPerSecond = table.Get("speed", 0f) * 480f; break; case "unit-spawner": var spawner = GetObject(name); From d8b59bbebd8a9b1f88ca8b82dea2002ee61f7f8a Mon Sep 17 00:00:00 2001 From: Dale McCoy <21223975+DaleStan@users.noreply.github.com> Date: Sat, 9 Nov 2024 02:07:52 -0500 Subject: [PATCH 6/6] feat: Show the 'spoiling' information for captive spawners. --- Yafc.Model/Data/DataClasses.cs | 17 +++++++++++++++++ .../Data/FactorioDataDeserializer_Entity.cs | 14 ++++++++++++++ Yafc/Widgets/ImmediateWidgets.cs | 2 +- Yafc/Widgets/ObjectTooltip.cs | 17 ++++++++++++++++- changelog.txt | 2 +- 5 files changed, 49 insertions(+), 3 deletions(-) diff --git a/Yafc.Model/Data/DataClasses.cs b/Yafc.Model/Data/DataClasses.cs index 45efd4f5..e0f1e180 100644 --- a/Yafc.Model/Data/DataClasses.cs +++ b/Yafc.Model/Data/DataClasses.cs @@ -487,6 +487,23 @@ public float Power(Quality quality) public int size { get; internal set; } internal override FactorioObjectSortOrder sortingOrder => FactorioObjectSortOrder.Entities; public override string type => "Entity"; + /// + /// Gets the result when this entity spoils (possibly , if the entity burns out with no replacement), + /// or if this entity doesn't spoil. + /// + public Entity? spoilResult => getSpoilResult?.Value; + /// + /// Gets the time it takes for a base-quality entity to spoil, in seconds, or 0 if this entity doesn't spoil. + /// + public float baseSpoilTime { get; internal set; } + /// + /// Gets the -adjusted spoilage time for this entity, in seconds, or 0 if this entity doesn't spoil. + /// + public float GetSpoilTime(Quality quality) => quality.ApplyStandardBonus(baseSpoilTime); + /// + /// The lazy store for getting the spoilage result, if this entity spoils. + /// + internal Lazy? getSpoilResult; public override void GetDependencies(IDependencyCollector collector, List temp) { if (energy != null) { diff --git a/Yafc.Parser/Data/FactorioDataDeserializer_Entity.cs b/Yafc.Parser/Data/FactorioDataDeserializer_Entity.cs index c5b28440..a6135363 100644 --- a/Yafc.Parser/Data/FactorioDataDeserializer_Entity.cs +++ b/Yafc.Parser/Data/FactorioDataDeserializer_Entity.cs @@ -561,6 +561,20 @@ void parseEffect(LuaTable effect) { if (entity.energy == voidEntityEnergy || entity.energy == laborEntityEnergy) { fuelUsers.Add(entity, SpecialNames.Void); } + + if (table.Get("production_health_effect", out LuaTable? healthEffect) && healthEffect.Get("not_producing", out float? lossPerTick)) { + entity.baseSpoilTime = (float)(table.Get("max_health") * -60 * lossPerTick.Value); + table.Get("dying_trigger_effect")?.ReadObjectOrArray(readDeathEffect); + + void readDeathEffect(LuaTable effect) { + if (effect.Get("type", "") == "create-entity" && effect.Get("entity_name", out string? spoilEntity)) { + entity.getSpoilResult = new(() => { + Database.objectsByTypeName.TryGetValue("Entity." + spoilEntity, out FactorioObject? spoil); + return spoil as Entity; + }); + } + } + } } private float EstimateArgument(LuaTable args, string name, float def = 0) => args.Get(name, out LuaTable? res) ? EstimateNoiseExpression(res) : def; diff --git a/Yafc/Widgets/ImmediateWidgets.cs b/Yafc/Widgets/ImmediateWidgets.cs index f074287e..95508a7f 100644 --- a/Yafc/Widgets/ImmediateWidgets.cs +++ b/Yafc/Widgets/ImmediateWidgets.cs @@ -96,7 +96,7 @@ public static void BuildFactorioObjectIcon(this ImGui gui, IFactorioObjectWrappe gui.DrawIcon(qualityRect, quality.icon, SchemeColor.Source); } - if (gui.isBuilding && obj.target is Item { baseSpoilTime: > 0 }) { + if (gui.isBuilding && obj.target is Item { baseSpoilTime: > 0 } or Entity { baseSpoilTime: > 0 }) { Vector2 size = new Vector2(displayStyle.Size / 2.5f); Rect spoilableRect = new Rect(gui.lastRect.TopLeft, size); diff --git a/Yafc/Widgets/ObjectTooltip.cs b/Yafc/Widgets/ObjectTooltip.cs index b44633f0..d5e61870 100644 --- a/Yafc/Widgets/ObjectTooltip.cs +++ b/Yafc/Widgets/ObjectTooltip.cs @@ -192,7 +192,7 @@ private void BuildCommon(FactorioObject target, ImGui gui) { {EntityEnergyType.SolidFuel, "Solid fuel energy usage: "}, }; - private static void BuildEntity(Entity entity, Quality quality, ImGui gui) { + private void BuildEntity(Entity entity, Quality quality, ImGui gui) { if (entity.loot.Length > 0) { BuildSubHeader(gui, "Loot"); using (gui.EnterGroup(contentPadding)) { @@ -240,6 +240,21 @@ private static void BuildEntity(Entity entity, Quality quality, ImGui gui) { } } + float spoilTime = entity.GetSpoilTime(quality); // The spoiling rate setting does not apply to entities. + if (spoilTime != 0f) { + BuildSubHeader(gui, "Perishable"); + using (gui.EnterGroup(contentPadding)) { + if (entity.spoilResult != null) { + gui.BuildText($"After {DataUtils.FormatTime(spoilTime)} of no production, spoils into"); + gui.BuildFactorioObjectButtonWithText(new ObjectWithQuality(entity.spoilResult, quality), iconDisplayStyle: IconDisplayStyle.Default with { AlwaysAccessible = true }); + } + else { + gui.BuildText($"Expires after {DataUtils.FormatTime(spoilTime)} of no production"); + } + tooltipOptions.ExtraSpoilInformation?.Invoke(gui); + } + } + if (entity.energy != null) { string energyUsage = EnergyDescriptions[entity.energy.type] + DataUtils.FormatAmount(entity.Power(quality), UnitOfMeasure.Megawatt); if (entity.energy.drain > 0f) { diff --git a/changelog.txt b/changelog.txt index d539e349..4bff6df8 100644 --- a/changelog.txt +++ b/changelog.txt @@ -23,7 +23,7 @@ Date: - Add the remaining research triggers and the mining-with-fluid research effect to the dependency/milestone analysis. - Explain what to do if Yafc fails to load a mod. - - (SA) Add spoiling time and result to item tooltips. Add a clock overlay to icons for perishable items. + - (SA) Add spoiling time and result to tooltips. Add a clock overlay to icons for perishable items/entities. Internal changes: - Using the LuaContext after it is freed now produces a better error. ----------------------------------------------------------------------------------------------------------------------