Skip to content

Commit

Permalink
Show total output for scrap (#338)
Browse files Browse the repository at this point in the history
When drawing a recipe with several item inputs or outputs, the UI will
add synthetic [I] or [O] items, representing the total sushi input or
output:

![image](https://github.com/user-attachments/assets/36434894-c80d-4688-823d-0586b027ecb3)

The default count is three, but it can be changed in the preferences:

![image](https://github.com/user-attachments/assets/3f1d6265-bc07-412c-8342-83bb7d42735a)

These synthetic items can't be linked, but you can set a fixed count for
them if you want a particular number of sushi-belts of production or
consumption.
  • Loading branch information
shpaass authored Oct 31, 2024
2 parents bb37a39 + 6bc35a4 commit b57cbfa
Show file tree
Hide file tree
Showing 8 changed files with 90 additions and 14 deletions.
5 changes: 2 additions & 3 deletions Yafc.Model/Data/DataClasses.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ public void FallbackLocalization(FactorioObject? other, string description) {

public int CompareTo(FactorioObject? other) => DataUtils.DefaultOrdering.Compare(this, other);

public virtual bool showInExplorers => true;
public bool showInExplorers { get; internal set; } = true;
}

public class FactorioIconPart(string path) {
Expand Down Expand Up @@ -304,6 +304,7 @@ public abstract class Goods : FactorioObject {
public FactorioObject[] miscSources { get; internal set; } = [];
public Entity[] fuelFor { get; internal set; } = [];
public abstract UnitOfMeasure flowUnitOfMeasure { get; }
public bool isLinkable { get; internal set; } = true;

public override void GetDependencies(IDependencyCollector collector, List<FactorioObject> temp) => collector.Add(production.Concat(miscSources).ToArray(), DependencyList.Flags.Source);

Expand Down Expand Up @@ -361,8 +362,6 @@ public class Special : Goods {
public override string type => isPower ? "Power" : "Special";
public override UnitOfMeasure flowUnitOfMeasure => isVoid ? UnitOfMeasure.None : isPower ? UnitOfMeasure.Megawatt : UnitOfMeasure.PerSecond;
internal override FactorioObjectSortOrder sortingOrder => FactorioObjectSortOrder.SpecialGoods;
public override bool showInExplorers => !isResearch;

public override void GetDependencies(IDependencyCollector collector, List<FactorioObject> temp) {
if (isResearch) {
collector.Add(Database.technologies.all.ToArray(), DependencyList.Flags.Source);
Expand Down
2 changes: 2 additions & 0 deletions Yafc.Model/Data/Database.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ public static class Database {
public static Dictionary<string, List<Fluid>> fluidVariants { get; internal set; } = null!;
public static Goods voidEnergy { get; internal set; } = null!;
public static Goods researchUnit { get; internal set; } = null!;
public static Goods itemInput { get; internal set; } = null!;
public static Goods itemOutput { get; internal set; } = null!;
public static Goods electricity { get; internal set; } = null!;
public static Recipe electricityGeneration { get; internal set; } = null!;
public static Goods heat { get; internal set; } = null!;
Expand Down
11 changes: 8 additions & 3 deletions Yafc.Model/Model/ProductionTableContent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -344,7 +344,7 @@ public Goods? fixedProduct {
if (value == null) {
_fixedProduct = null;
}
else if (recipe.products.All(p => p.goods != value)) {
else if (value != Database.itemOutput && recipe.products.All(p => p.goods != value)) {
// The UI doesn't know the difference between a product and a spent fuel, but we care about the difference
_fixedProduct = null;
fixedFuel = true;
Expand Down Expand Up @@ -612,13 +612,18 @@ public void Dispose() {
row.fixedBuildings *= row.parameters.recipeTime / oldParameters.recipeTime; // step 3, for fixed ingredient consumption
}
else if (row.fixedProduct != null) {
if (row.recipe.products.SingleOrDefault(p => p.goods == row.fixedProduct, false) is not Product product) {
if (row.fixedProduct == Database.itemOutput) {
float oldAmount = row.recipe.products.Where(p => p.goods is Item).Sum(p => p.GetAmountPerRecipe(oldParameters.productivity)) / oldParameters.recipeTime;
float newAmount = row.recipe.products.Where(p => p.goods is Item).Sum(p => p.GetAmountPerRecipe(row.parameters.productivity)) / row.parameters.recipeTime;
row.fixedBuildings *= oldAmount / newAmount; // step 3, for fixed combined production amount
}
else if (row.recipe.products.SingleOrDefault(p => p.goods == row.fixedProduct, false) is not Product product) {
row.fixedBuildings = 0; // We couldn't find the Product corresponding to fixedProduct. Just clear the fixed amount.
}
else {
float oldAmount = product.GetAmountPerRecipe(oldParameters.productivity) / oldParameters.recipeTime;
float newAmount = product.GetAmountPerRecipe(row.parameters.productivity) / row.parameters.recipeTime;
row.fixedBuildings *= oldAmount / newAmount; // step 3, for fixed production amount
row.fixedBuildings *= oldAmount / newAmount; // step 3, for fixed individual production amount
}
}

Expand Down
4 changes: 4 additions & 0 deletions Yafc.Model/Model/Project.cs
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,10 @@ public class ProjectPreferences(Project owner) : ModelObject<Project>(owner) {
/// The scale to use when drawing icons that have information stored in their background color, stored as a ratio from 0 to 1.
/// </summary>
public float iconScale { get; set; } = .9f;
/// <summary>
/// The <see cref="Database.itemInput"/> and <see cref="Database.itemOutput"/> pseudo-items will be displayed at or above this ingredient/product count.
/// </summary>
public int minForTotalItems { get; set; } = 3;

protected internal override void AfterDeserialize() {
base.AfterDeserialize();
Expand Down
23 changes: 23 additions & 0 deletions Yafc.Parser/Data/FactorioDataDeserializer_Context.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ internal partial class FactorioDataDeserializer {
private readonly Special electricity;
private readonly Special rocketLaunch;
private readonly Special researchUnit;
private readonly Item totalItemOutput;
private readonly Item totalItemInput;
private readonly EntityEnergy voidEntityEnergy;
private readonly EntityEnergy laborEntityEnergy;
private Entity? character;
Expand All @@ -53,6 +55,19 @@ Special createSpecialObject(bool isPower, string name, string locName, string lo
return obj;
}

Item createSpecialItem(string name, string locName, string locDescr, string icon) {
Item obj = GetObject<Item>(name);
obj.factorioType = "special";
obj.locName = locName;
obj.locDescr = locDescr;
obj.iconSpec = [new FactorioIconPart(icon)];
obj.isLinkable = false;
obj.showInExplorers = false;
rootAccessible.Add(obj);

return obj;
}

electricity = createSpecialObject(true, SpecialNames.Electricity, "Electricity", "This is an object that represents electric energy",
"__core__/graphics/icons/alerts/electricity-icon-unplugged.png", "signal-E");
fuels.Add(SpecialNames.Electricity, electricity);
Expand All @@ -62,6 +77,8 @@ Special createSpecialObject(bool isPower, string name, string locName, string lo

voidEnergy = createSpecialObject(true, SpecialNames.Void, "Void", "This is an object that represents infinite energy", "__core__/graphics/icons/mip/infinity.png", "signal-V");
voidEnergy.isVoid = true;
voidEnergy.isLinkable = false;
voidEnergy.showInExplorers = false;
fuels.Add(SpecialNames.Void, voidEnergy);
rootAccessible.Add(voidEnergy);

Expand All @@ -70,6 +87,7 @@ Special createSpecialObject(bool isPower, string name, string locName, string lo
researchUnit = createSpecialObject(false, SpecialNames.ResearchUnit, "Research",
"This represents one unit of a research task.", "__base__/graphics/icons/compilatron.png", "signal-L");
researchUnit.isResearch = true;
researchUnit.showInExplorers = false;
Analysis.ExcludeFromAnalysis<CostAnalysis>(researchUnit);

generatorProduction = CreateSpecialRecipe(electricity, SpecialNames.GeneratorRecipe, "generating");
Expand All @@ -84,6 +102,9 @@ Special createSpecialObject(bool isPower, string name, string locName, string lo

voidEntityEnergy = new EntityEnergy { type = EntityEnergyType.Void, effectivity = float.PositiveInfinity };
laborEntityEnergy = new EntityEnergy { type = EntityEnergyType.Labor, effectivity = float.PositiveInfinity };

totalItemInput = createSpecialItem("item-total-input", "Total item consumption", "This item represents the combined total item input of a multi-ingredient recipe. It can be used to set or measure the number of sushi belts required to supply this recipe row.", "__base__/graphics/icons/signal/signal_I.png");
totalItemOutput = createSpecialItem("item-total-output", "Total item production", "This item represents the combined total item output of a multi-product recipe. It can be used to set or measure the number of sushi belts required to handle the products of this recipe row.", "__base__/graphics/icons/signal/signal_O.png");
}

private T GetObject<T>(string name) where T : FactorioObject, new() => GetObject<T, T>(name);
Expand Down Expand Up @@ -120,6 +141,8 @@ private void ExportBuiltData() {
Database.allSciencePacks = [.. sciencePacks];
Database.voidEnergy = voidEnergy;
Database.researchUnit = researchUnit;
Database.itemInput = totalItemInput;
Database.itemOutput = totalItemOutput;
Database.electricity = electricity;
Database.electricityGeneration = generatorProduction;
Database.heat = heat;
Expand Down
29 changes: 29 additions & 0 deletions Yafc/Windows/PreferencesScreen.cs
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ private static void DrawGeneral(ImGui gui) {
}
}

float textBoxHeight; // measure the height of a text box, for use when drawing the minForTotalItems input box.
using (gui.EnterRow()) {
gui.BuildText("Reactor layout:", topOffset: 0.5f);
if (gui.BuildTextInput(settings.reactorSizeX + "x" + settings.reactorSizeY, out string newSize, null, delayed: true)) {
Expand All @@ -155,8 +156,36 @@ private static void DrawGeneral(ImGui gui) {
settings.reactorSizeY = sizeY;
}
}
textBoxHeight = gui.lastRect.Height;
}

string ioItemMessage = "The I and O items represent the total item input or item output of a recipe row.\nRecipes with at least this many item ingredients or item products will show the pseudo-items after all their real ingredients/products.";
using (gui.EnterRowWithHelpIcon(ioItemMessage)) {
float captionWidth = gui.GetTextDimensions(out _, "Minimum recipe ingredients or products").X;
using (gui.EnterFixedPositioning(captionWidth, 0, new())) { // the height will grow to fit the controls
gui.BuildText("Minimum recipe ingredients or products");
using (gui.EnterRow(0)) {
// Allocate the horizontal space now but draw the icons first, so the text can be drawn vertically centered.
Rect textRect = gui.AllocateTextRect(out _, "to display the ", TextBlockDisplayStyle.Default());
gui.BuildFactorioObjectButton(Database.itemInput, ButtonDisplayStyle.Default);
gui.BuildFactorioObjectButton(Database.itemOutput, ButtonDisplayStyle.Default);
textRect.Height = gui.lastRect.Height;
gui.DrawText(textRect, "to display the ");
gui.BuildText(" summary items");
}
}
float spacing = (gui.lastRect.Height - textBoxHeight) / 2;
using (gui.RemainingRow().EnterFixedPositioning(0, gui.lastRect.Height, new())) {
gui.AllocateSpacing(spacing - .5f); // draw the box vertically centered, rather than top-aligned (without this) or full height (without EnterFixedPositioning)
if (gui.BuildIntegerInput(prefs.minForTotalItems, out int newValue2)) {
prefs.RecordUndo().minForTotalItems = newValue2;
gui.Rebuild();
}
}
}

gui.AllocateSpacing();

if (gui.BuildCheckBox("Dark mode", Preferences.Instance.darkMode, out bool newValue)) {
Preferences.Instance.darkMode = newValue;
Preferences.Instance.Save();
Expand Down
25 changes: 17 additions & 8 deletions Yafc/Workspace/ProductionTable/ProductionTableView.cs
Original file line number Diff line number Diff line change
Expand Up @@ -536,6 +536,11 @@ public override void BuildElement(ImGui gui, RecipeRow recipe) {
grid.Next();
view.BuildGoodsIcon(gui, goods, link, amount, ProductDropdownType.Ingredient, recipe, recipe.linkRoot, HintLocations.OnProducingRecipes, variants);
}
if (recipe.fixedIngredient == Database.itemInput || recipe.Ingredients.Count() >= Project.current.preferences.minForTotalItems) {
grid.Next();
view.BuildGoodsIcon(gui, recipe.hierarchyEnabled ? Database.itemInput : null, null, recipe.Ingredients.Where(i => i.Goods is Item).Sum(i => i.Amount),
ProductDropdownType.Ingredient, recipe, recipe.linkRoot, HintLocations.None);
}
}
grid.Dispose();
}
Expand All @@ -552,6 +557,11 @@ public override void BuildElement(ImGui gui, RecipeRow recipe) {
grid.Next();
view.BuildGoodsIcon(gui, goods, link, amount, ProductDropdownType.Product, recipe, recipe.linkRoot, HintLocations.OnConsumingRecipes);
}
if (recipe.fixedProduct == Database.itemOutput || recipe.Products.Count() >= Project.current.preferences.minForTotalItems) {
grid.Next();
view.BuildGoodsIcon(gui, recipe.hierarchyEnabled ? Database.itemOutput : null, null, recipe.Products.Where(i => i.Goods is Item).Sum(i => i.Amount),
ProductDropdownType.Product, recipe, recipe.linkRoot, HintLocations.None);
}
}
grid.Dispose();
}
Expand Down Expand Up @@ -712,7 +722,7 @@ private enum ProductDropdownType {
}

private void CreateLink(ProductionTable table, Goods goods) {
if (table.linkMap.ContainsKey(goods)) {
if (table.linkMap.ContainsKey(goods) || !goods.isLinkable) {
return;
}

Expand Down Expand Up @@ -966,13 +976,12 @@ void dropDownContent(ImGui gui) {
"Nested tables can have its own separate set of links";
gui.BuildText(goodsNestLinkMessage, TextBlockDisplayStyle.WrappedText);
}
else {
else if (goods.isLinkable) {
string notLinkedMessage = goods.locName + " production is currently NOT linked. This means that YAFC will make no attempt to match production with consumption.";
gui.BuildText(notLinkedMessage, TextBlockDisplayStyle.WrappedText);
}

if (gui.BuildButton("Create link").WithTooltip(gui, "Shortcut: right-click") && gui.CloseDropdown()) {
CreateLink(context, goods);
if (gui.BuildButton("Create link").WithTooltip(gui, "Shortcut: right-click") && gui.CloseDropdown()) {
CreateLink(context, goods);
}
}
}
#endregion
Expand Down Expand Up @@ -1170,7 +1179,7 @@ private void BuildGoodsIcon(ImGui gui, Goods? goods, ProductionLink? link, float
case GoodsWithAmountEvent.LeftButtonClick when goods is not null:
OpenProductDropdown(gui, gui.lastRect, goods, amount, link, dropdownType, recipe, context, variants);
break;
case GoodsWithAmountEvent.RightButtonClick when goods is not null && (link is null || link.owner != context):
case GoodsWithAmountEvent.RightButtonClick when goods is not null and not { isLinkable: false } && (link is null || link.owner != context):
CreateLink(context, goods);
break;
case GoodsWithAmountEvent.RightButtonClick when link?.amount == 0 && link.owner == context:
Expand Down Expand Up @@ -1421,7 +1430,7 @@ protected override void BuildContent(ImGui gui) {
}

private static void AddDesiredProductAtLevel(ProductionTable table) => SelectMultiObjectPanel.Select(
Database.goods.all.Except(table.linkMap.Where(p => p.Value.amount != 0).Select(p => p.Key)), "Add desired product", product => {
Database.goods.all.Except(table.linkMap.Where(p => p.Value.amount != 0).Select(p => p.Key)).Where(g => g.isLinkable), "Add desired product", product => {
if (table.linkMap.TryGetValue(product, out var existing)) {
if (existing.amount != 0) {
return;
Expand Down
5 changes: 5 additions & 0 deletions changelog.txt
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@
// Internal changes:
// Changes to the code that do not affect the behavior of the program.
----------------------------------------------------------------------------------------------------------------------
Version:
Date:
Features:
- Add pseudo-items representing a recipe's total sushi-inputs and sushi-outputs.
----------------------------------------------------------------------------------------------------------------------
Version: 2.1.0
Date: October 29th 2024
Features:
Expand Down

0 comments on commit b57cbfa

Please sign in to comment.