JasonLandbridge / CircuitHUD-V2

https://mods.factorio.com/mod/CircuitHUD-V2
The Unlicense
4 stars 4 forks source link

Quality support #9

Open emk opened 1 week ago

emk commented 1 week ago

(Since people are interested, let's make this a real issue.)

How quality works. Factorio 2.0 adds an (optional) quality mechanism. If you insert "quality modules" in a machine, it will occasionally produce higher quality outputs. For example, a furnace with quality mods might produce:

  1. Common iron plates.
  2. Uncommon iron plates.
  3. Rare iron plates.
  4. Epic iron plates.
  5. Legendary iron plates.

The game treats these as five entirely different items. You can't make steel, for example, by mixing 4 common and 1 uncommon iron plate. You can either make common steel using 5 common iron plates, or uncommon steel using 5 uncommon iron plates. (And of course you can put quality mods in your steel furnace, for a random chance of jumping up a tier.)

Supporting quality in Circuit HUD. There are a number of things that would need to happen. Let's break these down into categories. First, the essentials:

Finally, there's an advanced feature possible here:

I think Cricuit HUD would be a ridiculously useful tool for monitoring high-quality production lines, which involve large numbers of similar items and feedback loops.

If I have time this weekend, I'll look at the mod API and see if I can start roughing out a PR. But if anyone else is interested, please go ahead!

emk commented 1 week ago

OK, some notes from the API:

Signals. If we look up .signals on a circuit network like this:

/c helpers.write_file('quality-signals.json', serpent.block(game.surfaces[5].find_entities_filtered{name='hud-combinator'}[1].get_circuit_network(2).signals))

...we get an array of signals in ItemIDAndQualityIDPair format:

  {
    count = 707,
    signal = {
      name = "quality-module-3"
    }
  },
  {
    count = 384,
    signal = {
      name = "quality-module-3",
      quality = "uncommon"
    }
  },
  {
    count = 63,
    signal = {
      name = "quality-module-3",
      quality = "rare"
    }
  },

If quality is omitted, that means it's common.

Filters. If I'm reading the docs right, these should look something like:

{
  name = "quality-module-3',
  quality = 'uncommon',
  comparator = '≥'
}

But name is optional, so we may get filters like:

{
  quality = 'uncommon',
  comparator = '≥'
}

Or we may see filters like:

{
  name = "quality-module-3',
}

Presumably, if we make sure that we recognize these values when returned from the API, and we pass them to the right API calls, we should be able to get most things working pretty quickly.

Other things we may need to do. We might also need to:

emk commented 1 week ago

Huh, the really hard part about all of this seems to be rendering the signal images with both a number and a quality?

LuaGuiElement is currently used to display each signal, using type = "sprite-button". This can display the sprite and a number, but not a quality. Or you could do this:

table[table_name].add {
    type = "choose-elem-button",
    elem_type = "signal",
    signal = { type = signal_type, name = signal_name, quality = "uncommon" },
    -- sprite = const.SIGNAL_TYPE_MAP[signal_type] .. "/" .. signal_name,
    -- quality = "uncommon",
    number = signal.count,
    style = network_styles[i],
    tooltip = signal_name_map[signal_type][signal_name].localised_name
}

...but that displays the quality and ignores number.

Using Control-F6 on the combinator windows shows that they have some nice C++ widgets for displaying signals, but they're not exported to Lua. Similarly, the logistics buttons in the inventory screen, and the filter button in the splitter UI seem to be using options that aren't available to Lua.

So displaying filters and signals actually seems like the hardest part of this.

emk commented 1 week ago

I still haven't found a way to get signal.signal.quality an signal.count onto the same icon. The best I have found is this:

local signal_quality = signal.signal.quality or "common"

...

table[table_name].add {
    type = "label",
    caption = "[item=" .. signal_name .. ",quality=" .. signal_quality .. "] " .. signal.count,
    tooltip = signal_name_map[signal_type][signal_name].localised_name
}

...which gives us:

image

(Well, that first quality indicator is wrong, but that was a bug in my code.)

This is going to have horizontal alignment issues as the numbers change, and it doesn't match the standard C++ UI widgets that Factorio uses internally:

image

But so far, labels are the best way I've found to display signals with both quantities and counts from Lua.

How do people feel about some version of the Lua UI with counts next to icons?

JasonLandbridge commented 1 week ago

Pinging @hgschmie for his input

I transferred most of the maintaining of this mod to @hgschmie since I quit Factorio but would love to see this mod live on.

About the counts next to icons, is this a missing mod feature in the API for these types of icons? Are there maybe other mods that have solved this issue from which you can steal the solution?

Ideally, this feature would be implemented without breaking existing functionality or have it be an optional experimental feature that can be enabled in the settings.

Either way, this sounds like a great addition! And my compliments for your detailed and extensive write-up!

emk commented 1 week ago

About the counts next to icons, is this a missing mod feature in the API for these types of icons? Are there maybe other mods that have solved this issue from which you can steal the solution?

I spent several hours last night digging through modding APIs, the Factorio data directory, other mods' source code, etc. I also tried lots of experiments with LuaGuiElement, looking for undocumented features. I used the built-in GUI inspector to see how Factorio did it, and discovered that in every case, they were using C++ GUI elements that aren't available in Lua.

I have not been able to find any way to get both quality and count displayed as a single icon, like the game's C++ code does. If it's there, it's exceptionally well hidden.

The problem seems to be that quality is implemented about 25% in C++ and 75% in an optional Lua mod. The UI bits are all in the engine itself, and very few of them seem to be exported to Lua. I imagine this will get fixed in several months. But for now, I can't do any better than you see above—quality on the icon, and a count next to it.

Ideally, this feature would be implemented without breaking existing functionality or have it be an optional experimental feature that can be enabled in the settings.

Obviously, we shouldn't break anyone's setup or save game, ever.

But at the same time, not displaying quality, or only optionally displaying quality, feels like it's simply a bug? "Uncommon iron plate" is a completely different signal than "Iron plate". Essentially, quality levels create completely new items that can't be used in place of common items. So the signals are completely distinct. If they're present, we should always display them with quality information.

Complex filters. On another note, it doesn't look like the complex filters used in splitters, things like "iron plates of quality uncommon or better" are actually available to the Lua API. at all. So it's probably better to omit these kinds of complex filters for now, and wait for API support.

So my proposed feature list is now:

emk commented 1 week ago

OK, here's a basic prototype! This is the best-looking UI that I can figure out how to create with the current modding API:

image

And here are the code changes I made. They're really simple. (Unfold to view.)

Code changes ```diff diff --git a/gui/hud-gui.lua b/gui/hud-gui.lua index aff48e4..4b7760b 100644 --- a/gui/hud-gui.lua +++ b/gui/hud-gui.lua @@ -13,12 +13,12 @@ local gui_hud = {} -- Checks if the signal is allowed to be shown based on the filters set for this HUD Combinator -- @returns if signal is allowed to be shown -local function filter_signal(signals, name) - if table_size(signals) == 0 then +local function filter_signal(signals_filter, signal_name, signal_quality) + if table_size(signals_filter) == 0 then return true end - for _, value in pairs(signals) do - if value.name == name then + for _, value in pairs(signals_filter) do + if value.name == signal_name and value.quality == signal_quality then return true end end @@ -162,6 +162,7 @@ function gui_hud.render_signals(hud_combinator, parent_gui, max_columns, signals for j, signal in pairs(networks[i].signals) do local signal_type = signal.signal.type or 'item' local signal_name = signal.signal.name + local signal_quality = signal.signal.quality -- Check if any signal is meant to hide everything if hide_signal_detected or signal_name == const.HIDE_SIGNAL_NAME then @@ -170,13 +171,22 @@ function gui_hud.render_signals(hud_combinator, parent_gui, max_columns, signals end -- Check if this signal should be shown based on filtering - if common.short_if(should_filter, filter_signal(signals_filter, signal_name), true) then + if common.short_if(should_filter, filter_signal(signals_filter, signal_name, signal_quality), true) then + base_tooltip = signal_name_map[signal_type][signal_name].localised_name + if signal_quality == nil then + caption = "[item=" .. signal_name .. "] " .. signal.count + tooltip = base_tooltip + else + caption = "[item=" .. signal_name .. ",quality=" .. signal_quality .. "] " .. signal.count + -- Some languages may prefer a different order here, but I don't know if there's a + -- localized template for that which we could use + tooltip = {"", prototypes.quality[signal_quality].localised_name, " ", base_tooltip} + end + table[table_name].add { - type = "sprite-button", - sprite = const.SIGNAL_TYPE_MAP[signal_type] .. "/" .. signal_name, - number = signal.count, - style = network_styles[i], - tooltip = signal_name_map[signal_type][signal_name].localised_name + type = "label", + caption = caption, + tooltip = tooltip, } signal_count = signal_count + 1 end ```

No data migration is necessary; the existing filter code actually saves and loads quality information correctly.

I could submit this as a PR, if people are happy with this! But if someone wants to try to find better UI options, I am very much open to suggestions!

JasonLandbridge commented 1 week ago

Looks good! Please make a PR and add a setting option with "Enable experimental item quality display" or something better you can think off, and then I'm happy to merge

emk commented 1 week ago

(Looking at this in game some more, I think we may also need to change the table to have fewer columns when displaying things like this.)

JasonLandbridge commented 1 week ago

There is a setting to change columns(rows): image

emk commented 1 week ago

Ah, excellent!

In addition to changing the width there, we would want to adjust the calculation:

width = (max_columns_allowed * (36 + 4)) + 40

...to use 52 instead of 36, to account for the wider grid cells.

Doing that, then manually adjusting Max HUD items per row to 4, and using format_number to format the numbers, we get this:

image

This seems to be pretty reasonable, if we want to properly display quality signals using the current mod APIs. I'm not sure if we can do much better without changes to Factorio itself.

I'll send a PR this weekend, which will give me to time figure out how to add a preference for switching between the old-style and the new-style signal displays.

JasonLandbridge commented 1 week ago

Great! Thanks for all the effort!

hgschmie commented 2 days ago

I did some work on CircuitHUD today (did you notice that you could not build it on a space station?). I will look at the PR and merge the changes into a new PR for 2.2.0.

I think the discrepancy between the icons available in lua and the C++ code is a bug / issue; I will see if I can get someone from Wube to fix this.

I will probably push 2.1.0 out and then make these changes a 2.1.1 or a 2.2.0

[@JasonLandbridge That every push to master is a new release is good and bad. Can we make that github action manually triggerable (but only by owner/collaborators on the repo)?

JasonLandbridge commented 2 days ago

[@JasonLandbridge That every push to master is a new release is good and bad. Can we make that github action manually triggerable (but only by owner/collaborators on the repo)?

Done! Can you check if you can run the action?

hgschmie commented 2 days ago

I have posted https://forums.factorio.com/viewtopic.php?f=28&t=122689 about the "sprite button with quality and number". Usually the Wube folks are pretty responsive in that forum. Worst case, we get "nah, won't do that", in that case we can still merge this PR.

hgschmie commented 2 days ago

about the quality icons. You can actually get pretty close with sprite-button:

Screenshot 2024-11-23 at 22 28 29

The five chests represent a chest each with a different quality. If you hover over them, they do show the correct element tooltip with

diff --git a/gui/hud-gui.lua b/gui/hud-gui.lua
index da57270..bdb0807 100644
--- a/gui/hud-gui.lua
+++ b/gui/hud-gui.lua
@@ -13,12 +13,12 @@ local gui_hud = {}

 -- Checks if the signal is allowed to be shown based on the filters set for this HUD Combinator
 -- @returns if signal is allowed to be shown
-local function filter_signal(signals, name)
+local function filter_signal(signals, signal)
        if table_size(signals) == 0 then
                return true
        end
        for _, value in pairs(signals) do
-               if value.name == name then
+               if value.name == signal.signal.name and value.quality == signal.signal.quality then
                        return true
                end
        end
@@ -180,14 +180,25 @@ function gui_hud.render_signals(hud_combinator, parent_gui, max_columns, signals
                                end

                                -- Check if this signal should be shown based on filtering
-                               if common.short_if(should_filter, filter_signal(signals_filter, signal_name), true) then
-                                       table[table_name].add {
+                               if common.short_if(should_filter, filter_signal(signals_filter, signal), true) then
+                                       local button = {
                                                type = "sprite-button",
                                                sprite = const.SIGNAL_TYPE_MAP[signal_type] .. "/" .. signal_name,
                                                number = signal.count,
                                                style = network_styles[i],
-                                               tooltip = signal_name_map[signal_type][signal_name].localised_name
+                                               tooltip = signal_name_map[signal_type][signal_name].localised_name,
                                        }
+
+                                       if signal_type == 'item' then
+                                               button.elem_tooltip = {
+                                                       type = 'item-with-quality',
+                                                       name = signal_name,
+                                                       quality = signal.signal.quality,
+                                               }
+                                       end
+
+                                       table[table_name].add(button)
+

is all that is needed for that. If we can figure out a way to actually display the icon correctly, then this would work really nicely...

emk commented 17 hours ago

If we can figure out a way to actually display the icon correctly, then this would work really nicely...

I did a lot of digging to find a way to get both the item count and the quality to display visually. As of two weeks ago, I couldn't find anything in the mod API, or in other mods. The GUI debugging tools show that icon+count+quality is displayed using non-exported C++ widgets.

Getting an upstream fix is ideal. But displaying quality is extremely useful by mid-game.