Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improvements to the GUI Framework #13

Open
Afforess opened this issue May 12, 2016 · 15 comments
Open

Improvements to the GUI Framework #13

Afforess opened this issue May 12, 2016 · 15 comments

Comments

@Afforess
Copy link
Owner

The GUI is a fertile ground for stdlib. Some broad suggestions include:

  • GUI Templates (create look-alike elements from a template)
  • Give every element callbacks (the single event the game provides is unwieldy)
  • Widget Groups (Label + Button)

Forum Suggestions: https://forums.factorio.com/viewtopic.php?f=96&t=23181&p=158743#p158740

Significant effort needs to be invested in making icon styles much easier to use, and in general making the gui api less opaque and difficult to learn or maintain.

@credomane
Copy link
Contributor

I decided to play around a bit with this. Ended up making a special event system for GUI events. Take a look and see what you think. Still working on it but so far so good.

This is the branch with my WIP code https://github.com/credomane/FactorioMods_Stdlib/tree/guiupdates

Mostly duplicated the Event code to Gui.Event. Then refactored (abused) that to support per gui_element_pattern handlers per event.

@Afforess
Copy link
Owner Author

Looks like a solid start. When I wrote the beginnings of the GUI module, there was only one GUI event, but Factorio 0.13 added a few more, so a completely separate event registry makes sense.

I'm curious if you have any other suggestions for improvements. The pattern support for events is a small improvement, but dealing with GUI elements still sucks; I haven't come up with any workable alternatives either.

@credomane
Copy link
Contributor

credomane commented Jul 19, 2016

Personally, I find making the GUI's in Factorio not that bad. Nothing like dealing with the freaking giant monolithic function the event handlers become instantly. Had an very rough idea how to solve it for my mod but I came looking to stdlib for an easier way first. Found there wasn't one but looking at the Event code I instantly knew how to combine my rough idea with your Event module. The GUI event wrapper/subsystem/thing I've added has made dealing with GUI elements events super easy. Almost jQuery easy, IMO. I believe that Give every element callbacks can be scratched out. Once/if you deem my code worthy of being pulled I'll make a pull request. :)

Here is a bunch of sample code and snippets for you or anyone that likes such things for how to use something. I know I do.

game.player[1].gui.top.add({type="checkbox", name="mytestelement", state = true, caption = "test"})
function testhandler(event)
    Game.print_all(event.name .. " state is now " .. tostring(event.state))
end

--All of these work and at the same time too.
Gui.on_checked_state_changed("mytestelement", testhandler)
Gui.on_checked_state_changed("mytest", anotherhandler)
Gui.on_checked_state_changed("my", function(event) doSomething.tm() end)

--Replacing a handler is as simple as 
Gui.on_checked_state_changed("my", newHandler)

--As I was typing this sample code I realized I should allow removing a handler like this so I added it.
Gui.on_checked_state_changed("mytest", nil)
Gui.on_checked_state_changed("my", nil)

--Support for any new defines.events.on_gui_* [and current] events can be immediately done like this
-- until a proper Gui.on_* helper can be created.
--Example new event on_gui_dragged
Gui.Event.register(defines.events.on_gui_dragged, "gui_element_pattern", myDraggedHandler);
Gui.Event.remove(defines.events.on_gui_dragged, "gui_element_pattern");

--if for some reason the "fake" event is missing something (future factorio version additions) then use event._event to get the real original event.

[edit]
If I get any ideas for anything else I'll be sure to code it up for you to look over. With this library being "the" Factorio Standard Library I don't want to prematurely create pull requests until my code is looked over and given the OK. Doubt either of us want to support a broken implementation of a feature. haha

Also as far as I can tell Gui.on_click() should be fully backwards compatible with the previous implementation.

@credomane
Copy link
Contributor

credomane commented Jul 19, 2016

Some quick ideas for easing GUI creation...I think jQuery (again for my inspiration) style search would be nice, perhaps? Even possible or too cumbersome for Lua and/or Factorio's implementation? I'm still way too used to WoW's Lua implementation and API. Factorio devs could probably take some things from WoW from a UI API standpoint. While WoW's was far from perfect the UI API was fairly powerful once you figured it out.

Here's a theoretical snippet of my Gui idea.

--Returns a special Gui table with a list of found elements. Searches gui.left, gui.top, gui.center by default
Gui.find_elements({name="myElement"}) -- returns elements matching name exactly. (regExp support?)
Gui.find_elements({position=LuaElement}) -- find ALL elements under LuaElement even children's children! Should probably have a better name than position.
Gui.find_elements({parent=LuaElement}) -- return only direct children of LuaElement
Gui.find_elements({type="button"}) -- return all buttons
--Mix-n-match
Gui.find_elements({type="button",parent=LuaElement}) -- return all buttons with specified parent
Gui.find_elements({type="button",parent="myElement"}) -- return all buttons with a parent named "myElement"

--Passing a plain string is internally changed to {name=parameter_value}
Gui.find_elements("myElement")

--Perhaps overload Gui to Gui.find_elements()?
Gui({name="myElement"})

--Add a child element to each element found
Gui({name="myElement"}).add("{factorio luaElement table info here"})

--Destroy all the found elements
Gui({name="myElement"}).destroy()

--Create an event handler for all the found elements
Gui({name="myElement"}).on_click(myHandler)

[edit]
After sleeping on it. I have an idea for GUI creation. I'll mock up a gist later for opinions.
Here is that gist https://gist.github.com/credomane/db116cf51b6e44a088315ae7fdaf464c

@OvermindDL1
Copy link

Actually if you want to model things on web frameworks (not a bad idea at all), might I recommend the Elm style? It handles monolithic GUI's (in compiles to javascript) via HTML by a very succinct and easy to handle and expand subscription/event system. It is declarative instead of imperative so it is much harder to break things accidentally as you use it.

@credomane
Copy link
Contributor

credomane commented Jul 19, 2016

I'm open to anything. My intended goal for the Gui mock-up isn't a full featured gui manager but a ease-of-use wrapper to the underlining Factorio API. From my understanding the mock-up I provided is declarative, right? The mock up being the gist I provided in a edit. The find_elements is nice idea but I provided a poor design for it, I think.

As for the elm style? What is that? Don't think I've ever used that.

@OvermindDL1
Copy link

OvermindDL1 commented Jul 19, 2016

Elm is one of those languages that compiles to Javascript, been around 3 or 4 years though has recently reached 1.0 and has gotten very popular lately, it is kind of a much easier to use Haskell, strongly typed, with its compiler doing a lot of optimizations on the code. One of those languages where if it compiles you can be sure it works. Ignore the Elm language though and look at its concepts where were the same model to make things like React and Redux (the high performance and crash-proof libraries that facebook released a few years ago for javascript). In a high level it essentially works like this (in Lua'ese, the concepts are important here, not the language):

There are 2 data concepts:

  1. Model -> A user defined type, can be anything, in LUA parlance it could be an integer, string, table, whatever (table would be most common no doubt); it is entirely user defined. This type is used to 'Store' your GUI state. This would store fantastically well into the global table (or heck, someone could write their entire mod in this style and the global table would 'be' their model, that is what I would personally do).
  2. Msg -> Also a user defined tag, most often a tagged union, but could be an integer, string, whatever. This is what will cause 'Changes' to your GUI state. In LUA this would most commonly be a string (since string comparison are so fast in LUA, but could use integers in a 'define'-like way too, or could use a table, or whatever, it is all user defined so whatever-the-heck-they-want-for-their-use-case). Messages could be wrapped in an (internal implementation defined, not visible or used by the users except for wrapping it up) Command or a Subscription.

There are 4 callbacks:

  1. init -> Called once at startup, this takes no input arguments and returns a tuple of a (Model, Command). The command just being any user Msg wrapped in an implementation defined Cmd call. In Lua I'd do it like:

    local init = function()
    
      local my_model = {
        something = 42,
        more = "hi",
        even_more = { hi = "there" }
      }
    
      local my_cmds = Cmd.batch{
        Cmd{id="MyInitialMessage"} -- As an example I'm using a table as my Msg type with an `id` key to define the type as a string
        -- If there is only one command then Cmd.batch can be omitted and you can return a Cmd directly, they return the same type anyway.
      }
    
      return my_model, my_cmds
    end
  2. view -> This takes the user Model and returns a data structure definition for the GUI. The return value is compared to the previous and any changes will be reflected onto the on-screen version. Perhaps it could be something like this in Lua:

    gv = Gui.ViewHelpers
    
    local view = function(my_model)
      if my_model.enabled == false then return end
    
      return gv.screen{
        onscreen_buttons = {
          (my_model.my_research_unlocked and gv.button{ -- Lua's ternery 'operator' >.>  Probably should use actual tests, but brevity for examples
            name = "Do Something!",
            onClick = {id="do_something"} -- Hey, its my Msg type again, should probably make a constructor for it to simplify it
            } or gv.none),
        },
        model = my_model.show_dialog and gv.model{ -- As in an on-screen model, like a dialog box, maybe name it 'dialog', though 'model' is more universally common, or 'models' if it supports multiple models simultaneously
          title = "My dialog box!",
          gv.table{ -- Could do lots of different layout types instead of just the basic table...
            gv.row{
              gv.col{
                gv.text("The value is: " .. my_model.some_val)
              },
              gv.col{
                gv.input{
                  defaultValue = "",
                  onChange = {id="input_changed"} -- My Msg type again!
                }
              },
              gv.col{
                gv.button{
                  name = "Confirm and Close",
                  onClick = {id="confirm_close_dialog"} -- My Msg type is everywhere!
                }
              }
            }
          }
        }
      }
    end

    Ooo now that is bigger, but that defines an entire GUI, with a button on screen at the top that is only shown if a certain value on the model is true, it shows a GUI dialog box with a label with dynamic text, a text input, and a button to close it. However this is just the data definition, it does 'nothing' itself. The person using this will not know this part, but in the back-end those calls build up a simple table structured like how the above is structured. The backup would call the view callback anytime a message passes through (it is lightweight, changes only happen as necessary). In the backend when view is called it holds the old version of the view and takes the return from the view callback as the new. It then compares the old and new structures by element, keeping track of the 'id' of its location (which the user could specify above if they want, but almost never necessary in practice if well programmed) such as 0, 1, 2, etc... Dead simple. Every time it goes a layer deep it concates the parents with the new one so it can be "0.0.3" for example, this would be the ID of the gui element in Factorio. If it finds a change between the two then it submits an update to that changed GUI element in Factorio, updating the text, removing it, adding it, whatever. The 'onClick' handlers are stored in a table keyed on the gui element ID (so something like "2.0.4.1.6" or whatever) with a subtable of the interaction type (like 'click'). That table also has changes done on it if handlers exist one time but not another, all part of the standard diff updating step. When something is interacted with then you give a message of the given type and any other message data so something like an 'onClick' on an input mandates a table msg type and it will add a new key of, say, value with the current value state of the input element.

  3. update -> This callback takes a Msg and a Model and returns a Model and a command. This transforms the model using the given msg/event and returns a new model altered as necessary. In LUA it may look something like this:

    local update = function(my_msg, my_model)
      if     my_msg.id == "do_something" then
        my_model.show_dialog = !my_model.show_dialog -- Honestly this should be a 'new' model, but eh, LUA, mutate and break the everything!
        return my_model, cmd.none
      elseif my_msg.id == "input_changed" then
        my_model.some_val = my_msg.value
        return my_model, Cmd.none
      elseif my_msg.id == "confirm_close_dialog" then
        my_model.show_dialog = false
        return my_model, Cmd.Factorio.kill{entity=my_model.player[0]} -- What a twist!
      else
        log("Uhh, unhandled msg, is bad, sign of a bug: " .. my_msg.id)
      end
    end

    So the backend gets back the new model (great for debugging is printing the model state changes via a simple diff algorithm over time), and any commands that want to be run. Commands will be talked about shortly.

  4. subscriptions -> This callback takes the Model and returns 'Subscriptions', like Commands but for linking in instead of sending out. This could be used like:

    local subscriptions = function(my_model)
      return Sub.batch{
        StdLib.Factorio.onEvent("on_load"), -- No data passed in, no need to translate to internal state
        StdLib.Factorio.onEvent("on_configuration_changed"), -- This is exactly idential to the next call
        StdLib.Factorio.onEvent("on_configuration_changed", function(data) -- This is identical to the above, the body here is what it does by default unless you want to modify it to your custom message type
          data.id = "on_configuration_changed"
          return data
          end)
      }
    end

    So subscriptions listen to an 'external' event (such as generated by Factorio script calls, by another mod perhaps, etc...), translate the message to the internal message format, then is passed in to update call.

And Commands are used to send a command to the outside system (or even to send another message internally).

And of course it could be registered like:

StdLib.Program{
  init = init,
  view = view,
  update = update,
  subscriptions = subscriptions,
}

So that is the overall external interface for it. An entire mod could be written in such a way and it would be performant, smaller, and easier to write without bugs (and with some debug-time-enabled checks the back-end could verify the front-end state for various things to catch mis-types and such), but even for just a GUI it is a great format that scales well. Normal apps have view calls that branch to other functions based on various criteria in model, can loop to make lists of things, can filter the data easily, the subscriptions are even dynamic and can be enabled/disabled as necessary (the backend would do the work of registering or unregistering with Factorio as the events vanish or not from one subscription state to the other). This is the style that Elm, React/Redux/Flow, mercury(sp?) and many others take on Javascript that is the fastest updating of any other frameworks. The state management (even without the view callback) is also fantastic for stability of a program due to translation of data from one state to the next.

@credomane
Copy link
Contributor

The Factorio GUI API is very small and rigid. Build anything other than an almost one to one API to API easy-to-use wrapper seems like a lot of effort for little return. I've tried using react.js in the past and found it to be nothing but frustratingly difficult to use. I just didn't 'get' it; I still don't. Found jQuery shortly there after and never looked back. Your sample code reminds me very much of reactjs. So assuming that, if I was to undertake making an elm-style gui lib for stdlib I'd quite poorly as I can't even understand the elm model in the first place. Not saying my jquery style would be great either. Just that if I was to make both a jquery and reactjs gui lib then the jquery style would come out infinitely better.

This is @Afforess' project so we'd have wait for a decision on this anyway. If the decision is for elm-like I'd take a crack at it at least. Both as developer and a user of stdlib.

@Afforess
Copy link
Owner Author

@credomane @OvermindDL1

I thoroughly enjoyed following this discussion and intentionally didn't respond immediately so as to not interrupt the brainstorming.

@OvermindDL1

My initial wish would be that Factorio gui system look like something sane. Even if the Factorio GUI API was modeled after something like JS/CSS that would be a huge improvement. Unfortunately, it isn't. I've come at the problem from a lot of angles, even really extreme ones where stdlib replaces the game object at runtime with one that is instrumented to intercept all gui object creation, and hooks metatables into them.

The main intractable issue I can't see a solution past is the one of closures and persistence. I don't see the Elm model, or any other existing frameworks I am aware of bridging this issue. The problem, as I see it, is that you can't create GUI event handlers dynamically (for example, at the same time you create a GUI object), because:

  1. Handlers aren't persisted, but re-registered each time a mod is initialized
  2. Even if handlers were persisted by the mod, in global, this would cause the event callback to be persisted, which would make it impossible to update/migrate users to newer releases of the mods. Saves would drag along older callbacks, carrying forward old bugs into new releases.

I can't see past this. The web has no analogous situation, because when a web page is created, its not persisted, and even if you decide to save the html, css, and js that all composes a webpage, when you re-open it, your browser re-executes it from an empty initial state. It does not persist the page content.*

@credomane

Everything you've written so far seems solid. One minor suggestion I have is that GUI.find_elements could be shortened to just GUI.find. Elements is implied by the GUI context.

*localstorage, browser cookies are not content.

@credomane
Copy link
Contributor

credomane commented Jul 20, 2016

even really extreme ones where stdlib replaces the game object

Extreme indeed. I'm laughing so hard at the ridiculousness of it and yet, I'm seriously considering the feasibility of going through with it. A mad/evil scientist type laugh. I need a minute....ok. All better. I would think the only way to really do it and do it properly would be to "demand" that require "stdlib.Game" as the very first line in mod's control.lua. Would it be stdlib.game instead to match the case of the game object? That is irrelevant...moving on.

I never thought about the event handler persistence in Factorio with any stdlib api style the gui portion becomes. That does pose an interesting issue. Do we register an internal on_load handler to re-register missing event handlers? So then the mod dev can just trust that the event listener will always be there until they specifically remove it? That is the type of thing that factorio's mod_configuration_changed event is meant to handle I would say. As long as it was made clear in the docs that stdlib registered events would persist across game loads I think it would be fine. Also, what do were do if we do have an internal on_load but we are fighting the stdlib user over it because they are, by their rightful choice, manually doing script.on_load(handler) instead of using stdlib.Event? Suppose I should say what can we do? Replace the script object, too? lol

Currently my way of doing it (read: likely the wrong way) is to have a function that is called by on_init and on_load. This function registers all of my GUI handlers. The way I have the gui event system working only one handler per pattern. So multiple registers of the same pattern only use the most recently registered handler and due to factorio's design if the gui can't be seen then it doesn't actually exist so the gui event subsystem will never see the pattern I told it to listen for. So having them floating around "permanently" without the gui element is ok. My way poses an issue if I did things like Gui.top(example).on_click(handler) as there is no way to make those persist across loads. Perhaps don't allow at all as it is a Factorio feasibility and reliability issue?

Everything you've written so far seems solid

Ok. I'll issue a pull request then. This is for the Gui.Event subsystem only...that I kinda already started using in my "refactor-in-progress" mod too.

I haven't even started on a Gui.find_elements it was just and idea I threw out there. Figured once the gui api proper came into being it would naturally be molded into that.

...This took a surprising amount of time to type. I saw your post when it was 38 seconds old....now it is just an hour old.

[edit]
No idea why I used code markdown instead of the quote markdown.

@Afforess
Copy link
Owner Author

even really extreme ones where stdlib replaces the game object

Extreme indeed. I'm laughing so hard at the ridiculousness of it and yet, I'm seriously considering the feasibility of going through with it. A mad/evil scientist type laugh.

Replacing the game object is a lot easier than it sounds. _G.game = {}. Okay, that's probably bad news for any mod you execute that it, but that is all the code you need.

The reason I have not done that is not the difficulty, or questionable nature, but the maintenance cost. The maintenance cost of replacing/hot-patching the existing game API is huge, and will make upgrading from Factorio 0.13 to 0.14 and beyond much more challenging. So replacing the existing game API is always an option, but an option I prefer to leave in the deck unless all else fails.

As long as it was made clear in the docs that stdlib registered events would persist across game loads I think it would be fine.

How exactly do you see mod upgrades/migrations working? Does a modder have to explicitly write migration code? How complex is it to update the event handlers? I think it's worth remembering the Factorio modder community is largely novice programmers, and many have a lot of trouble with the existing prototype migration included in base Factorio... anything more complicated than that will be too difficult for any sort of practical adoption, or worse, will hurt stdlib as confused modders write buggy code.

I'm open to persisting handlers, but tread lightly. I think its critical this remains intuitive.

@credomane
Copy link
Contributor

The maintenance cost of replacing/hot-patching the existing game API is huge, and will make upgrading from Factorio 0.13 to 0.14 and beyond much more challenging.

That is where I ended up too, in a way it would be the Factorio version of updating forge for new Minecraft releases. Minus the obfuscation hurdles, of course.

How exactly do you see mod upgrades/migrations working? Does a modder have to explicitly write migration code?

They would... Would be something like this striped down version from my mod. I'm not arguing for it. Just how it would be done. The more I think about it the less I like the idea of attempting it.

--on_configuration_changed()
function mod.configuration_changed(event)
    for modName,modTable in pairs(event.mod_changes) do
        if modName == MOD_FULLNAME then
            mod.doMigration(modTable.old_version, modTable.new_version);
        end
    end
end

function mod.doMigration(oldVersion, newVersion)
    print_all({"Updater", "Version changed from " .. oldVersion .. " to " .. newVersion .. "."});

    if oldVersion > newVersion then
        print_all({"Updater", "Version downgrade detected. I can't believe you've done this."});
        return;
    end

    if oldVersion < "0.0.7" then --or whatever version the handlers were changed in.
        --remove old handlers here.
    end

How complex is it to update the event handlers?

I was considering a Gui.Event.purge() which would unceremoniously remove all gui events but I'm not convinced of the necessity for it. Then you would just register what you wanted.

@OvermindDL1
Copy link

OvermindDL1 commented Jul 20, 2016

The Elm style is indeed a bit... inverted from what most people are used to, but once you get it (with a proper example instead of a long description it is substantially more obvious) it makes a lot more sense. :-)

The main intractable issue I can't see a solution past is the one of closures and persistence. I don't see the Elm model, or any other existing frameworks I am aware of bridging this issue. The problem, as I see it, is that you can't create GUI event handlers dynamically (for example, at the same time you create a GUI object), because:

Actually that is the interesting thing about this style, even in javascript Elm does not register event handlers on the objects but rather it registers a global handler when they bubble-up through the DOM. Elm (and react) can run server-side as well, and that affects its style for a few reasons (described shortly). In Factorio it would work very well because:

  1. Handlers aren't persisted, but re-registered each time a mod is initialized

This fits this model precisely. The Model is the only place where information is persisted, there are no handlers in it, no GUI-code, no anything else but pure 'state' data. When a game is 'loaded' the execution path would be like:

  1. Do not run init since that is only run on first run when the Model does not exist, if it already exists then init is skipped.

  2. Then run the usual events, such as on_configuration_change and so forth (backend calling: model = update(FactorioEvent("on_configuration_change", eventData), model) or so, though through the subscription system)

  3. Once the game is about to start (probably the on_load Factorio event would be best for this as it does not mutate globals, if on_load is called after on_configuration_change, I'm unsure actually...), this is when you start the diffing process, I.E call the user-mod callbacks like this:

     -- This code chunk would be inside the back-end msg processing
     -- Neither of this are or should be persisted, ever
     local gui_state_old = gui_state
     gui_state = view(model) -- You might even want to check model before and after in debug mode to make sure they do not edit it since LUA does not have immutable data structures
     local gui_state_changes = Gui.diff_states(gui_state_old, gui_state)
     unregister_events(gui_state_changes.msgs_removed) -- You can of course do these next 4 in the diff_states call itself, but I like separating concerns.
     register_events(gui_state_changes.msgs_added)
     remove_gui_elements(gui_state_changes.gui_removed)
     add_gui_elements(gui_state_changes.gui_added)
    
     local subs_state_old = subs_state
     subs_state = subscriptions(model)
     local subs_state_changes = StdLib.FactorioEvents.diff_states(subs_state_old, subs_state)
     unregister_events(subs_state_changes.events_removed)
     register_events(subs_state_changes.events_added)

    This as you can see the global state has no data added or remove or persisted during any of these calls, thus when the game starts you just get the 'snapshot' of how the GUI should be defined, and via the diff testing (against nothing, an empty object) it rebuilds it all in-game, then as long as no changes happen during messages (the only time it is retested, and there are ways to change that too) then the diffs remain the same and no changes are made in Factorio.

  1. Even if handlers were persisted by the mod, in global, this would cause the event callback to be persisted, which would make it impossible to update/migrate users to newer releases of the mods. Saves would drag along older callbacks, carrying forward old bugs into new releases.

The handlers should absolutely not ever be persisted in such a system, ever ever ever. This style (and Elm thanks to this style) can migrate versions very easily, like Elm can even migrate from version-to-version without ever restarting (live code updates!). The 'version_change' is just another message given to update so the user can test the version and migrate the model from the old version to the new as they wish (or just return it if no changes to the model). This system fits Factorio-modding to an absolute. :-)

Just remember, only the model/state is serialized, that is completely user controlled, not even the back-end system should store anything in state, it is a stateless system other than the completely user-controlled Model.

Let's try a simple mod in this style, how about something that hurts all players every time any craft anything in their inventory with it hurting more and more each (encourages machine crafting) (too simple of an example for this, but to show the concepts):

-- GLOBALS CONFIG START

-- Low enough to encourage no buildings, high enough to give a good buffer before reaching better assembler tech
local harm_per_item = 0.5 -- LUA really needs a 'const' type...

-- GLOBALS CONFIG END
StdLib = require("./stdlib/stdlib")
Cmd = require("./stdlib/cmd")
Sub = require("./stdlib/sub")

local init = function()
  return { itemscrafted = 0 } -- I like to make a wrapping table so it is easy to extend later if the need arises
end


local handlers = { -- I like to put my handlers in a table and index into it instead of doing if blah then elseif chains

  on_player_crafted_item = function(msg, model)
    model.itemscrafted += msg.item_stack.count
    return model, Cmd.Factorio.AllPlayers.Damage(model.itemscrafted * harm_per_item)
  end

}


local update = function(msg, model)
  return handlers[msg.id] -- technically if msg.id does not exist then I have a bug as all cases that I register for should be handled, so this naive version is fine, even for release
end


local subscriptions = function(model)
  return Sub.Factorio.Events("on_player_crafted_item")
end


StdLib.Mod{
  model = global,
  init = init,
  update = update,
  subscriptions = subscriptions
}

So the first time the mod loads (there is an empty 'global' for example) then the 'Mod' runner runs init to populate the global. The game could stop at this point for as much as anything cares and that would be saved out.

In the backend, probably 'on_load' or so, then call (view if it existed, which it does not for this example, but then call) subscriptions, which in this case just always wants to listen to the "on_player_crafted_item" event, so the backend sees that this is a new registration (since subscriptions had not been called yet in this game run) and thus registers that event in Factorio (the callback of that I will get to shortly).

At this point since subscriptions should not mutate state then everything is still serializable out.

The player soon crafts an item, thus the backend event handler that was registered for "on_player_crafted_item" gets called, it then calls update passing in the crafted msg and the mods last model state and takes the return value as the new model for the first element, and a command (or batched multiple commands, or Cmd.none for no command), and in this case it gets the command table that Cmd.Factorio.AllPlayers.Damage(model.itemscrafted * harm_per_item) returns, probably something like {id="factorio", cmd="players_dmg_all", value=0.5}, which the backend then runs through the Command processor, just a table of calls to make back to Factorio for example, if the id was 'msg' then it is a user message and it will be passed in to update so updates can call updates, or other things.

It is a system that is designed to scale arbitrarily, easily, and safely, so it would be an especially huge boon to mods with lots of script functionality, especially like GUI mods. In a Factorio setup you could probably get rid of the whole Cmd.Factorio module as they player could just the factorio API directly, though update is 'supposed to be' pure, should not modify the game state, only mutate the internal mod global model and return commands to run.

It is easy to extend this system as well, for example someone could make a Cmd that can run things a certain amount of ticks in the future, or on a pattern, and if no ticks are happening it could unregister "on_tick", it could be something like this:

-- File: stdlib/cmd/timer.lua
StdLib = require("./stdlib/stdlib")
Cmd = require("./stdlib/cmd")
sub = require("./stdlib/sub")


local MSG_ONESHOT = "oneshot"


local init = function()
  return { oneshots = {} }
end


local update = function(msg, model)

  if msg.id == "on_tick" then -- Keep on_ticks simple!
    local at_time_list = model.oneshot[game.tick] or []
    if #at_time_list > 0 then
      local cmds = []
      for i = 1, #at_time_list do
        cmds[#cmds+1] = Cmd.msg(msg)
      end
      model.oneshot[game.tick] = nil
      return model, Cmd.batch(cmds)
    else
      return model, Cmd.none
    end

  else if msg.id == MSG_ONESHOT then
    if msg.in_ticks <= 0 then
      return model, Cmd.msg(msg) -- Registered in the past?  Run it now then!
    end
    local at_time = game.tick + msg.in_ticks
    local at_time_list = model.oneshot[at_time] or []
    at_time_list[#at_time_list+1] = msg
    model.oneshot[at_time] = at_time_list
    return model, Cmd.none

  end
end


local subscriptions = function(model)
  if #model.oneshots > 0 then
    return Sub.Factorio.Events("on_tick")
  else
    return Sub.none
  end
end


return {
  init = init,
  update = update,
  subscriptions = subscriptions,
  Cmds = {
    Oneshot = function(in_ticks, msg) return Cmd.msg{id=MSG_NAME, msg={id=MSG_ONESHOT, in_ticks=in_ticks, msg=msg}}
    }
}

Which could be used in the prior mod to, say, delay damage for one second by doing:

-- GLOBALS CONFIG START

-- Low enough to encourage no buildings, high enough to give a good buffer before reaching better assembler tech
local harm_per_item = 0.5 -- LUA really needs a 'const' type...
local harm_delay_ticks = 60

-- GLOBALS CONFIG END
StdLib = require("./stdlib/stdlib")
Cmd = require("./stdlib/cmd")
Sub = require("./stdlib/sub")
Timer = require("./stdlib/timer")


local init = function()
  return {
    itemscrafted = 0,
    timer = Timer.init() -- Explicit is better than implicit!
    } -- I like to make a wrapping table so it is easy to extend later if the need arises
end


local handlers = { -- I like to put my handlers in a table and index into it instead of doing if blah then elseif chains

  on_player_crafted_item = function(msg, model)
    model.itemscrafted += msg.item_stack.count
    return model, Timer.Cmds.Oneshot(harm_delay_ticks, Cmd.Factorio.AllPlayers.Damage(model.itemscrafted * harm_per_item))
  end

  timer = function(msg, model) -- Explicit about what is handled, implicit is *bad*
    return Timer.update(msg.timer, model.timer)
  end

}


local update = function(msg, model)
  return handlers[msg.id] -- technically if msg.id does not exist then I have a bug as all cases that I register for should be handled, so this naive version is fine, even for release
end


local subscriptions = function(model)
  return Sub.batch{
    Sub.Factorio.Events("on_player_crafted_item"),
    Sub.map{sub=Timer.subscriptions(model.timer), id="timer", msg={id="timer"}} -- `map` takes a subscription and 'maps' it into another message, in this case any `msg` for timer will be wrapped with `{id="timer", timer=TimerMsg}`, you could do the same via your own function handler, this is just a more simple method.
  }
end


StdLib.Mod{
  model = global,
  init = init,
  update = update,
  subscriptions = subscriptions
}

And badda boom we have an automatic 60 second delay to harming the player after crafting and it will register and unregister the on_tick as it is needed or not (as the Timer subscription function only asks for it when there are timers waiting). We can add in more Timer.Cmds.Oneshot as we wish and automatic registration happens only when needed. (This 'timer' module is stock included in most systems like this.)

This style is extremely powerful and every module in this style in the other languages follow this same format. All state is encapsulated and restricted to the model, even importing modules that require state require explicitely storing their state so you know exactly what is happening, where, and when.

Do note, I'm not necessarily pushing for this, rather I hope that the design can help push for a better GUI system (whatever that may be, and perhaps even a global modding style) in this library as it is highly useful.

@originalfoo
Copy link
Contributor

Could we use a HAML-ish DSL (let's call it FUI) for defining gui structure, and then a jquery-ish syntax (let's call it fuiQuery) for interacting with the gui?

FUI example

local myGui = [[
  dialog#name .title='title'
    vertical#options
       checkbox#foo .label='some text'
]]

Just a very rough example to give a jist of what it might look like. The benefits of this approach are:

  • Simple to write, read and comprehend
  • We can add our own meta elements (eg. 'vertical' is a flow that's set to vertical)
  • Once parsed, relatively simple to map to vanilla GUI API
  • Easy to see how it relates to fuiQuery

@Dandielo
Copy link

Dandielo commented Jan 13, 2017

Hey there, I've just stumbled over this library on the forums and I would like to share a Gui implementation I've done myself

For now it bases on lets say 'Gui pages'
A gui page is a table that defines a whole gui tree along with relations child <-> parent allowing automatically append a back button on the end

    gui.append
    {
        type = "button",
        name = "dytech-debug-button",
        open = "dytech-debug-flow",
        parent = "dytech-menu",
        caption = { "dytech-gui.debug-button" },
    }

    gui.create 
    {
        type = "flow",
        name = "dytech-debug-flow",
        direction = "horizontal",
        childs = 
        {
            {
                type = "frame",
                name = "dytech-debug-menu",
                parent = "dytech-menu",
                direction = "vertical",
                caption = { "dytech-gui.debug-title" },

                childs = 
                { 
                    {
                        type = "button",
                        name = "debug-research-all-button",
                        caption = { "dytech-gui.debug-research-all-button" }
                    },
                    {
                        type = "button",
                        name = "debug-toggle-cheats-button",
                        caption = { "dytech-gui.debug-toggle-cheats-button" }
                    },
                    {
                        type = "button",
                        name = "debug-enable-all-button",
                        caption = { "dytech-gui.debug-enable-all-button" }
                    },
                }
            },
        }
    }

When it comes to event handling you can do this by just declaring a new function upon a special field / table

function gui.clicked.debug_research_all_button(event)
    local player_force = game.players[event.player_index].force
    player_force.research_all_technologies()
end
function gui.clicked.debug_toggle_cheats_button(event)
    game.players[event.player_index].cheat_mode = not game.players[event.player_index].cheat_mode
end

To make all this work you would need to call the gui.handle_gui_event function (during the gui event, obviously)

However besides that I've also added special events (but only for elements in that system) refresh and loaded

-- 
-- Sets the caption on the chunk pollution label
function core.gui.refresh.chunk_pollution(event)
    local pollution = 0
    local chunk_num = 0

    for chunk in game.surfaces.nauvis.get_chunks() do
        local tile_x = chunk.x * 32 + 16
        local tile_y = chunk.y * 32 + 16

        chunk_num = chunk_num + 1
        pollution = pollution + game.surfaces.nauvis.get_pollution { tile_x, tile_y }
    end
    
    -- Set the caption
    event.element.caption = { "dytech-gui.core-stats-chunk-pollution", string.format("%.2f", pollution / chunk_num) }
end

To make use of these events you would also need to call the gui.handle_time_events during the on_tick event.

Alongside this you can set a "default" gui page that will be displayed on the beginning and / or after some timeout has passed (this also needs to call the 'gui.handle_time_events' function)

And we got also simple configuration values with it

-- Idle timeout (after which the gui will reset to the base state: control.lua@196) 
-- * if you set '0' it will disable the gui reset
gui.idle_time = 600
gui.idle_timeout = { } -- Each player has it's own

-- Refresh timeout (after what time should elements get a 'refresh' event)
gui.refresh_time = 180
gui.refresh_timeout = 0

This system is used in my attempt to restore "DyTech" mods, you can get a glimpse on the gui system here: https://github.com/Dandielo/CORE-DyTech-Core/tree/master/scripts

It's quite good but needs still a but of tinkering and some changes as because not all events are handled yet

@Afforess Afforess added this to the Future Direction milestone May 22, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

5 participants