Factorio-Access / FactorioAccess

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

make it possible to redirect keyboard input to a UI module via "layers" #263

Open ahicks92 opened 1 week ago

ahicks92 commented 1 week ago

This replaces #207 and must be done after #262

We want a better UI. But that's a large project and it's not always clear what we might want. A better UI unlocks lots of things, for example giving mods a remote API to respond to us, reducing code duplication by well over 2x, so on. But this is a long plan and no one has the time to do it in one chunk.

But the good news is there is a shorter plan which also unlocks other capabilities if we need them: support a mechanism to redirect input to another module temporarily. We'll call these layers. The default layer is what we have today. Other UI stuff is other various layers, which can be enabled and disabled as needed and will "swallow" keyboard input if they want to. So, for example, the blueprint menu could stay as it is but be pulled to a file and wrapped in a layer swallowing wasd and [. For non-UI things, say better train building, other layers can come in to replace current functionality down the road..

The one tricky part is textboxes. We can treat those closing with layers too in the sense of redirecting that event, and just keep what we have today where we have a bunch of tricky manual names and renders. Later we have the flexibility to do better in more generic frameworks e.g. by generating the names dynamically with our unique id module.

To make use of it, find a menu tangled up in the events like we have today with the giant if trees of doom. Copy/paste all of that out of control.lua to a file. Wrap those functions in a layer. When opening the menu, set everything we do today (so that other old code doesn't break) and then activate the layer till it closes. This allows handling the menus one by one as better options become available to us, without breaking old stuff as we gradually improve the story.

LevFendi commented 6 days ago

The idea sounds nice overall but I am not sure on how to go about it. Can we consider a micro example with a simple keybind like WASD?

ahicks92 commented 6 days ago

You asking for a PR version of it or just some detailed pseudocode?

This isn't a unique idea, could probably also dig up docs for other things that do it. I believe pyglet does. Bevy used to do something similar but Bevy is Bevy so they replaced it with a version where only super-geniuses need apply, so that's not helpful. I know old xna docs used to do something like this in their tutorials somewhere.

Still in theory it's mechanical work so I could probably like throw together the blueprint menu or something as it were. Just not functional. In practice there is untangling from control.lua issues I'm sure, though we have to do that no matter what which is why I'm glossing over that part--it might suck but doesn't matter which plans you choose, it's got to get done.

LevFendi commented 6 days ago

I would appreciate the pseudocode of an example

ahicks92 commented 5 days ago

Ok, so in control.lua for the blueprint menu we have fa_blueprints.blueprint_menu_up(pindex) (we still need to do that rename for style but I digress). To benefit from layers we would do this:

-- Metatable magic, don't worry about it at this point.
-- In practice this is probably a class.
local BlueprintLayer = Layer.new()

Layer.register('fa-menu-up', function(event)
    fa_blueprints.blueprint_menu_up(pindex)
end)

-- Do the others.

Now, okay so we want to open it. That'd be something like this:

BlueprintLayer:open()
-- For legacy code.
-- We can also bake this into layers.
players[pindex].menu = "blueprint"

All the events now either hit the layer or "flow through" to things underneath. E.g. it's possible to check health or whatever.

And to close BlueprintLayer:close() and set global however for legacy. Then we move them one by one. When legacy's gone legacy's gone and we get rid of the global manipulation.

Now okay where's the benefit. The benefit long term is we probably come up with a nicely generic UI system. For example if we allow stacking these or whatever, or have a "tab layer" that manages opening/closing other layers when you press the tab key. But short term, we could invent something like this, which makes all the manual keyboard handling go away:

function simple_list_layer(declaration)
-- Magic.
end

local BlueprintLayer = simple_menu_layer({
    handler = blueprint_handler,
    options = {
        { result = "foo", label = "My Label" },
        ...
    }
})

-- The handler.
function blueprint_handler(result)
    if result == "foo" then handle_foo() end
    elseif ...
end

Which okay but Austin you just made a thing that does an if tree. Yes I did. Thank Factorio. However imagine if we had a "simple form" and everything is handled by the "simple form handler" and you just get a bundle of values when it updates or whatever to your handler (e.g. like formic). It's also possible to link callbacks if the callbacks are known statically, in the case of simple forms we could probably just inline them and require declaring them during load of control.llua and that'd be why the simple form is simple. But my favorite trick that we haven't used yet is:

local function_can_be_in_global = wrap_with_name('a unique string that never changes', function() ... end)

Which can make functions register (like metatables) so that we can e.g. pass them into the scheduler and so on as well rather than doing our current trick wherein we're using _env and strings and you can't schedule outside control.lua.

Plus don't forget field-ref exists, getting a setter for some field on something that's just a function(something, value) is just one line (which is why I wrote field-ref, we just never got far enough to use it heavily).

The last thing that doesn't go without saying but doesn't need an explanation: we can of course link renders to these, which may help solve our render bug problems. And the generic layers could by their nature get graphical GUIs too.

Obviously this needs a bit more plumbing (where is pindex?) but (1) getting into that seems like a waste given how simple it is and (2) I just got done doing all sorts of things and adding a number of helpers for scanner anyway.

Now the reason I assert this is worth it. Getting as far as being able to have a layer is just writing an obvious array of layers that you push and pop from, then making a mechanical change in control.lua to register custom events through the layer module so that the layer module gets to see our base registrations and wrap our handlers with the ones that look at layers. Then people can experiment or write further frameworks or whatever. It's mostly just:

function push(l) table.insert(layers, l) end
function pop() table.remove(layers, #layers) end

function run_event(name, event)
for i = #layers, 1, -1 do
    if layers[i].registrations[name] trhen layers[i].registrations[name](event)
    -- Or whatever else.
end
end

And initially we can just allow one only for example, though I think the stack might eventually be useful which is why I show it that way.