Factorio-Access / FactorioAccess

An accessibility mod for the video game Factorio, making the game accessible to the blind and visually impaired.
Other
22 stars 9 forks source link

make UI reusable and clean #207

Closed ahicks92 closed 1 month ago

ahicks92 commented 3 months ago

I mean for background read the code or be on Discord. Basically we don't have reusable UI and what we have now is...amusing. I promised a plan. This is a plan.

Background

Let's ignore how GUIs are implemented today. It's not useful to get into that mostly because it's slightly different everywhere. Instead, let's list the workflows we have, plus a couple things I personally want.

What we have:

It's also worth noting We cannot express the armor grid, because that's multi-cell items that can rotate. I think that we have to code that as a oneoff. See below for how.

The problems with what we have:

now I also said it'd be good if we could support two additional things:

So, plan time.

1. Get rid of the events problem.

Eph has been proposing a prioritized event handler that runs oinly one event, but it keeps getting bogged down and I at least am skeptical of integer priorities and the like. Let us therefore declare event handlers like this:

{
  ... Other events,
  {
    name = 'cursor-north', -- or whatever else.
    handler = handle_this,
    when = condition_fn, -- but see below.
  }
}

The condition handler is annoying but fopr the sake of this discussion take it as a given that we will do:

or_(item_in_hand("iron-plate"), gui_closed)

or and_, nested to arbitrary depth, and yes it would work with pindex. We can get into how this would be implemented if needed. The trailing _ is because leading _ usually means private in languages without proper privacy, and these functions are named for Lua keywords.

Just put events down in the order they should run, it runs down it, it hits them till it finds one that matches, it runs it. very simple. For debugging we just print out if there's ever more than one. This drops things like letting the player know there were two possibilities which Eph wanted, but today we can't even reliably fire only one when only one is relevant. Also writing conditions like that should let us pretty print what happened, if we really need to.

This ties into the rest because, as above, that ui_open bit. Do that, or alternatively just flag whether or not the event only implies when a UI is open, or whatever--the primary thing is that we'd then have a way to call only one event handler. Also this is a possible solution to #198 as the mod can record internal custom input events and then replay them inside lua if we wanted to go that far (detecting which are custom events is something I am 99% sure we can do without making you write it).

This part is fast to write but long to explain. Here comes the hard stuff.

New GUIs

new GUIs will consist of a table, one per widget type. This table will be built only from state in global. As seen below we won't support dynamic controls: you declare the GUI at the top level on load and that's it too bad sucks to be you.

To use a menu for example, it would have:

This menu would be declared effectively as a global variable. We would offer an object oriented menu builder. You would use that to divorce yourself from the format. There are a few parts that make this work:

One notable thing not described so far: how do controls handle dynamic changes? We will do that by allowing (some) controls to describe themselves with a function callback instead of a static definition. For example menus will need to be able to take a function which can return the genuine menu structure. As above, one thing a menu specifically gets (when possible) is key, which can be used to re-sync focus as items are added. For other controls such as grids, you'd need a function for dimensions and to get a slot, etc.

Now, time for nesting UIs, generic pieces like textboxes etc. What we don't need is a full, detailed, and robust stack. It would be nice but ultimately we have so far been able to fit everything in what we currently have, it's pretty nice modulo bugs and limitations, let's just make people keep doing that. To make this work, every time something calls ui_open we record which UI opened and then to go "back" is just to descend one level. The difference between this and the more generic solution: we won't allow cycles. No oepning a UI that's already open.

There is one final elephant in the room: textboxes. It's not practical to code up something which makes you declare a textbox to go with every menu item, so it'll be necessary to have a textbox-only GUI declaration path. We will make all text boxes the same. When a textbox is opened the control can pass additional arguments to be returned to itself in whatever fashion. When trbhe textbox closes to return to the GUI it's going to return to, it passes that along. To make that work fine we just need to be sure that users can never open a second GUI somehow, which is already impossible via the virtue of input handling but can be enforced simply buy closing all GUIs.

One final question wrt using it which may come up: don't menus have multiple kinds of things? yes. But ultimately only 4 of them:

That's small enough that we can bite the bullet and make the generic menu just understand it. Same with other controls. Realistically we should never need more, and 1 and 2 come for free (1 is obvious; 2 bec ause the only way it changes is via a handler, because the value must also be written). Similar philosophy applies to other controls, as we can keep things simple if we choose to do so.

2. State

This seems worth a section. For background Factorio supports global state which must be synced between all clients in a game, and local lua-vm specific state which is wiped out on restarts of the client. This is the breakdown of that.

In local state:

In global state, let's call this the "render state":

3. "timelines"

This is an OSS project so really just estimates of difficulty.

The event handler is idk, 2 hours. It's not that complicated. Add another hour for preparinjg. these steps are required: lift event handlers to named functions, even if this means making them global to avoid import cycles since most are in control.lua, write the logic, insert them into the table, convert their conditions into the condition expressions. Thbose condition exprtessions are just some closure tricks, not actually complex to set up (or_ itself returns a function, so on).

I think our next target should be some menu. Travel menu in this model is a bit...special. Not hard as such but not easy either. Maybe we target the blueprint menu fiorst to get something. I'm going to say this, sadly, is about a man-day but not complicated enough to be done in one wall clock day; there's a lot of experimentation and code shuffling and throwing thigns at the wall here to get the basic structure up and running.

Finally is the rest of it. That's the biggest unknown. We do things one by one as we can, we probably do the main inventory/crafting menus last, we do the ability to put UI components in tabs at this stage, and so on. Then just move things over as people have time, and always use it in new projects instead of the legacy patterns.

ahicks92 commented 2 months ago

This is effectively blocked on https://github.com/Factorio-Access/FactorioAccess/discussions/225

if we decide to not continue down the road of key overloads how we do them now, that entirely changes how we handle input here. Handling input blocks doing anything else for hopefully obvious rasons. Don't want to write what's proposed herre if it's possible to just avoid it.

ahicks92 commented 1 month ago

I'm closing this. I've got new job looming. Scanner took longer than expected. Etc. Plus we had various discussions since around input handling.

Instead we will do #262, followed by #263. This will unlock our ability to experiment and deal with this problem more gradually.