Skip to content

Commit

Permalink
Add shopping list display options (shpaass#280)
Browse files Browse the repository at this point in the history
This adds additional options for how the shopping list counts your
crafters and modules. Each radio button has a tooltip to clarify how
things will be counted.


![image](https://github.com/user-attachments/assets/d6f1a094-9059-431a-89e2-c0beec96d5cf)

The default is currently the previous behavior, but I'm tempted to
change the default to "Missing buildings" and "No buildings". That
selection shows where and by how much the required count exceeds the
built count, instead of showing the built count.
  • Loading branch information
shpaass authored Sep 11, 2024
2 parents 21cc8f9 + 4e2c968 commit 9a42bee
Show file tree
Hide file tree
Showing 6 changed files with 121 additions and 46 deletions.
30 changes: 24 additions & 6 deletions Yafc.UI/ImGui/ImGuiUtils.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using SDL2;

Expand Down Expand Up @@ -215,19 +216,36 @@ public static bool BuildCheckBox(this ImGui gui, string text, bool value, out bo
return false;
}

public static bool BuildRadioButton(this ImGui gui, string option, bool selected, SchemeColor color = SchemeColor.None) {
public static ButtonEvent BuildRadioButton(this ImGui gui, string option, bool selected, SchemeColor textColor = SchemeColor.None, bool enabled = true) {
if (textColor == SchemeColor.None) {
textColor = enabled ? SchemeColor.PrimaryText : SchemeColor.PrimaryTextFaint;
}
using (gui.EnterRow()) {
gui.BuildIcon(selected ? Icon.RadioCheck : Icon.RadioEmpty, 1.5f, color);
gui.BuildText(option, TextBlockDisplayStyle.WrappedText with { Color = color });
gui.BuildIcon(selected ? Icon.RadioCheck : Icon.RadioEmpty, 1.5f, textColor);
gui.BuildText(option, TextBlockDisplayStyle.WrappedText with { Color = textColor });
}
if (!enabled) {
return ButtonEvent.None;
}

return !selected && gui.OnClick(gui.lastRect);
ButtonEvent click = gui.BuildButton(gui.lastRect, SchemeColor.None, SchemeColor.None);
if (click == ButtonEvent.Click && selected) { return ButtonEvent.None; }
return click;
}

public static bool BuildRadioGroup(this ImGui gui, IReadOnlyList<string> options, int selected, out int newSelected, SchemeColor color = SchemeColor.None) {
public static bool BuildRadioGroup(this ImGui gui, IReadOnlyList<string> options, int selected, out int newSelected,
SchemeColor textColor = SchemeColor.None, bool enabled = true)
=> gui.BuildRadioGroup([.. options.Select(o => (o, (string?)null))], selected, out newSelected, textColor, enabled);

public static bool BuildRadioGroup(this ImGui gui, IReadOnlyList<(string option, string? tooltip)> options, int selected,
out int newSelected, SchemeColor textColor = SchemeColor.None, bool enabled = true) {
newSelected = selected;
for (int i = 0; i < options.Count; i++) {
if (BuildRadioButton(gui, options[i], selected == i, color)) {
ButtonEvent evt = BuildRadioButton(gui, options[i].option, selected == i, textColor, enabled);
if (!string.IsNullOrEmpty(options[i].tooltip)) {
evt.WithTooltip(gui, options[i].tooltip!);
}
if (evt) {
newSelected = i;
}
}
Expand Down
4 changes: 4 additions & 0 deletions Yafc/Utils/Preferences.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,10 @@ public void Save() {
/// - Your system has a very old graphics card that is not supported by Windows DX12
/// </summary>
public bool forceSoftwareRenderer { get; set; } = false;
/// <summary>
/// An opaque integer that the shopping list uses to store its display options. See the ShoppingListScreen properties that read and write this value.
/// </summary>
public int shoppingDisplayState { get; set; } = 3;

public void AddProject(string dataPath, string modsPath, string projectPath, bool expensiveRecipes, bool netProduction) {
recentProjects = recentProjects.Where(x => string.Compare(projectPath, x.path, StringComparison.InvariantCultureIgnoreCase) != 0)
Expand Down
2 changes: 1 addition & 1 deletion Yafc/Windows/MainScreen.PageListSearch.cs
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ void buildCheckbox(ImGui gui, string text, ref bool isChecked) {
void buildRadioButton(ImGui gui, string text, SearchNameMode thisValue) {
// All checkboxes except PageSearchOption.PageName search object names.
bool isObjectNameSearching = checkboxValues[1..].Any(x => x);
if (gui.BuildRadioButton(text, searchNameMode == thisValue, isObjectNameSearching ? SchemeColor.PrimaryText : SchemeColor.PrimaryTextFaint) && isObjectNameSearching) {
if (gui.BuildRadioButton(text, searchNameMode == thisValue, enabled: isObjectNameSearching)) {
searchNameMode = thisValue;
updatePageList();
}
Expand Down
106 changes: 88 additions & 18 deletions Yafc/Windows/ShoppingListScreen.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using Yafc.Blueprints;
Expand All @@ -7,13 +8,32 @@

namespace Yafc {
public class ShoppingListScreen : PseudoScreen {
private static readonly ShoppingListScreen Instance = new ShoppingListScreen();

private enum DisplayState { Total, Built, Missing }
private readonly VirtualScrollList<(FactorioObject, float)> list;
private float shoppingCost, totalBuildings, totalModules;
private bool decomposed = false;
private static DisplayState displayState {
get => (DisplayState)(Preferences.Instance.shoppingDisplayState >> 1);
set {
Preferences.Instance.shoppingDisplayState = ((int)value) << 1 | (Preferences.Instance.shoppingDisplayState & 1);
Preferences.Instance.Save();
}
}
private static bool assumeAdequate {
get => (Preferences.Instance.shoppingDisplayState & 1) != 0;
set {
Preferences.Instance.shoppingDisplayState = (Preferences.Instance.shoppingDisplayState & ~1) | (value ? 1 : 0);
Preferences.Instance.Save();
}
}

private readonly List<RecipeRow> recipes;

private ShoppingListScreen() => list = new VirtualScrollList<(FactorioObject, float)>(30f, new Vector2(float.PositiveInfinity, 2), ElementDrawer);
private ShoppingListScreen(List<RecipeRow> recipes) {
list = new VirtualScrollList<(FactorioObject, float)>(30f, new Vector2(float.PositiveInfinity, 2), ElementDrawer);
this.recipes = recipes;
RebuildData();
}

private void ElementDrawer(ImGui gui, (FactorioObject obj, float count) element, int index) {
using (gui.EnterRow()) {
Expand All @@ -23,31 +43,81 @@ private void ElementDrawer(ImGui gui, (FactorioObject obj, float count) element,
_ = gui.BuildFactorioObjectButtonBackground(gui.lastRect, element.obj);
}

public static void Show(Dictionary<FactorioObject, int> counts) {
float cost = 0f, buildings = 0f, modules = 0f;
Instance.decomposed = false;
Instance.list.data = counts.Select(x => (x.Key, Value: (float)x.Value)).OrderByDescending(x => x.Value).ToArray();
foreach (var (obj, count) in Instance.list.data) {
if (obj is Entity) {
buildings += count;
public static void Show(List<RecipeRow> recipes) => _ = MainScreen.Instance.ShowPseudoScreen(new ShoppingListScreen(recipes));

private void RebuildData() {
decomposed = false;

// Count buildings and modules
Dictionary<FactorioObject, int> counts = [];
foreach (RecipeRow recipe in recipes) {
if (recipe.entity != null) {
FactorioObject shopItem = recipe.entity.itemsToPlace?.FirstOrDefault() ?? (FactorioObject)recipe.entity;
_ = counts.TryGetValue(shopItem, out int prev);
int builtCount = recipe.builtBuildings ?? (assumeAdequate ? MathUtils.Ceil(recipe.buildingCount) : 0);
int displayCount = displayState switch {
DisplayState.Total => MathUtils.Ceil(recipe.buildingCount),
DisplayState.Built => builtCount,
DisplayState.Missing => MathUtils.Ceil(Math.Max(recipe.buildingCount - builtCount, 0)),
_ => throw new InvalidOperationException(nameof(displayState) + " has an unrecognized value.")
};
counts[shopItem] = prev + displayCount;
if (recipe.usedModules.modules != null) {
foreach ((Module module, int moduleCount, bool beacon) in recipe.usedModules.modules) {
if (!beacon) {
_ = counts.TryGetValue(module, out prev);
counts[module] = prev + displayCount * moduleCount;
}
}
}
}
else if (obj is Module module) {
}
list.data = [.. counts.Where(x => x.Value > 0).Select(x => (x.Key, Value: (float)x.Value)).OrderByDescending(x => x.Value)];

// Summarize building requirements
float cost = 0f, buildings = 0f, modules = 0f;
decomposed = false;
foreach ((FactorioObject obj, float count) in list.data) {
if (obj is Module module) {
modules += count;
}

else if (obj is Entity or Item) {
buildings += count;
}
cost += obj.Cost() * count;
}
Instance.shoppingCost = cost;
Instance.totalBuildings = buildings;
Instance.totalModules = modules;
_ = MainScreen.Instance.ShowPseudoScreen(Instance);
shoppingCost = cost;
totalBuildings = buildings;
totalModules = modules;
}

private static readonly (string, string?)[] displayStateOptions = [
("Total buildings", "Display the total number of buildings required, ignoring the built building count."),
("Built buildings", "Display the number of buildings that are reported in built building count."),
("Missing buildings", "Display the number of additional buildings that need to be built.")];
private static readonly (string, string?)[] assumeAdequateOptions = [
("No buildings", "When the built building count is not specified, behave as if it was set to 0."),
("Enough buildings", "When the built building count is not specified, behave as if it matches the required building count.")];

public override void Build(ImGui gui) {
BuildHeader(gui, "Shopping list");
gui.BuildText(
"Total cost of all objects: " + DataUtils.FormatAmount(shoppingCost, UnitOfMeasure.None, "¥") + ", buildings: " +
DataUtils.FormatAmount(totalBuildings, UnitOfMeasure.None) + ", modules: " + DataUtils.FormatAmount(totalModules, UnitOfMeasure.None), TextBlockDisplayStyle.Centered);
using (gui.EnterRow()) {
if (gui.BuildRadioGroup(displayStateOptions, (int)displayState, out int newSelected)) {
displayState = (DisplayState)newSelected;
RebuildData();
}
}
using (gui.EnterRow()) {
SchemeColor textColor = displayState == DisplayState.Total ? SchemeColor.PrimaryTextFaint : SchemeColor.PrimaryText;
gui.BuildText("When not specified, assume:", TextBlockDisplayStyle.Default(textColor), topOffset: .15f);
if (gui.BuildRadioGroup(assumeAdequateOptions, assumeAdequate ? 1 : 0, out int newSelected, enabled: displayState != DisplayState.Total)) {
assumeAdequate = newSelected == 1;
RebuildData();
}
}
gui.AllocateSpacing(1f);
list.Build(gui);
using (gui.EnterRow(allocator: RectAllocator.RightRow)) {
Expand Down Expand Up @@ -99,7 +169,7 @@ private void ExportBlueprintDropdown(ImGui gui) {

private Recipe? FindSingleProduction(Recipe[] production) {
Recipe? current = null;
foreach (var recipe in production) {
foreach (Recipe recipe in production) {
if (recipe.IsAccessible()) {
if (current != null) {
return null;
Expand Down
22 changes: 1 addition & 21 deletions Yafc/Workspace/ProductionTable/ProductionTableView.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1163,27 +1163,7 @@ private List<RecipeRow> GetRecipesRecursive(RecipeRow recipeRoot) {
return list;
}

private void BuildShoppingList(RecipeRow? recipeRoot) {
Dictionary<FactorioObject, int> shopList = [];
var recipes = recipeRoot == null ? GetRecipesRecursive() : GetRecipesRecursive(recipeRoot);
foreach (var recipe in recipes) {
if (recipe.entity != null) {
FactorioObject shopItem = recipe.entity.itemsToPlace?.FirstOrDefault() ?? (FactorioObject)recipe.entity;
_ = shopList.TryGetValue(shopItem, out int prev);
int count = MathUtils.Ceil(recipe.builtBuildings ?? recipe.buildingCount);
shopList[shopItem] = prev + count;
if (recipe.usedModules.modules != null) {
foreach (var module in recipe.usedModules.modules) {
if (!module.beacon) {
_ = shopList.TryGetValue(module.module, out prev);
shopList[module.module] = prev + (count * module.count);
}
}
}
}
}
ShoppingListScreen.Show(shopList);
}
private void BuildShoppingList(RecipeRow? recipeRoot) => ShoppingListScreen.Show(recipeRoot == null ? GetRecipesRecursive() : GetRecipesRecursive(recipeRoot));

private void BuildBeltInserterInfo(ImGui gui, float amount, float buildingCount) {
var prefs = Project.current.preferences;
Expand Down
3 changes: 3 additions & 0 deletions changelog.txt
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,11 @@ Date:
Features:
- Add OSX-arm64 build.
- Display link warnings in both the tooltips and the dropdowns.
- Add additional ways of counting buildings and modules when displaying the shopping list.
Bugfixes:
- Fixed recipes now become accessible when their crafter does.
Internal changes:
- Allow tooltips to be displayed when hovering over radio buttons.
----------------------------------------------------------------------------------------------------------------------
Version: 0.9.1
Date: September 8th 2024
Expand Down

0 comments on commit 9a42bee

Please sign in to comment.