Page 1 of 3

Dependencies: Are "optional requirements" possible at all?

Posted: Wed Jun 05, 2019 1:10 pm
by Pi-C
I'm working on my first mod, a compatibility mod.

There are 4 mods (A-D). Mod A is the target of my mod. Mod D is obsolete (not updated to 0.17), but has been forked to B and C. Both B and C provide an item (same look, different recipes/technology requirements) that I want to base an item from A on.

A is strictly required, so obviously, info.json should contain this line:

Code: Select all

"dependencies": ["A"]
Now I need a source to modify the target. It would be easy if there only was one source mod:

Code: Select all

"dependencies": ["A", "B"]
However, I have B and C. Given their similarity, it's usually unnecessary to have both installed, so a hard dependency on both would be wrong:

Code: Select all

"dependencies": ["A", "B", "C"]
How about optional dependencies?

Code: Select all

"dependencies": ["A", "?B", "?C"]
In this case, the dependency would also be fulfilled if either A, B, and C or just A were installed. The first is bad because usually only one of B and C is necessary; the second is bad because my mod isn't needed at all if neither B nor C are active.

Is there no way to do more complex stuff? Actually, I'm looking for something like

Code: Select all

require A and (B or C)
or even

Code: Select all

require A and (B xor C)
.

Re: Dependencies: Are "optional requirements" possible at all?

Posted: Wed Jun 05, 2019 1:16 pm
by Deadlock989
I don't believe there is. I might be wrong - I only just found out yesterday that you can have hidden optional dependencies by using (?).

If there isn't, your best bet is to detect whether the mods are present using mods[], and then do something if none of the optionals are actually present. You could halt the load with error() and a message directing the player to go and install the preferred mod. But this would also present the default option of disabling your mod, which isn't a terrible thing but a minor irritant. Other than that, you're left with chat messages on game load to warn that the mod isn't doing anything because none of its optional dependencies are present.

Re: Dependencies: Are "optional requirements" possible at all?

Posted: Wed Jun 05, 2019 1:41 pm
by eradicator
Deadlock989 wrote:
Wed Jun 05, 2019 1:16 pm
I only just found out yesterday that you can have hidden optional dependencies by using (?).
You're welcome.
____

@Pi-C:
Yea. You're in a shitty situation there. More complex depencies don't exist.
I personally don't think throwing error()s works, because "users" don't read error messages. Especially when the text you want to display is sandwiched between a built-in error-header and a stacktrace. So your best option is to simply do nothing if the configuration isn't what you need. And then hope people read the mod description. And hopefully you don't also have to deal with sitiations where B *and* C are installed?

Re: Dependencies: Are "optional requirements" possible at all?

Posted: Wed Jun 05, 2019 2:35 pm
by Pi-C
Deadlock989 wrote:
Wed Jun 05, 2019 1:16 pm
I don't believe there is. I might be wrong - I only just found out yesterday that you can have hidden optional dependencies by using (?).
That's a nice one! :-)
If there isn't, your best bet is to detect whether the mods are present using mods[], and then do something if none of the optionals are actually present. You could halt the load with error() and a message directing the player to go and install the preferred mod. But this would also present the default option of disabling your mod, which isn't a terrible thing but a minor irritant.
I guess disabling my mod would be OK -- no use loading it unless it does something, and it can't do anything if the other mods are missing.
Other than that, you're left with chat messages on game load to warn that the mod isn't doing anything because none of its optional dependencies are present.
I played with that, too, but I can't get it to work.

I can read game.active_mods from on_init() and on_configuration_changed(), so it's trivial to print a message to the player when a new save game is created or once when a save file is loaded that previously didn't contain the mod or when the configuration changed in some way. However, I would also need on_load(), which triggers every time a save file is loaded except for the instance when a mod is loaded into a save file that it previously wasn't part of -- but LuaGameScript is not available in this event.

No problem, I thought. Each mod also has access to its own instance of the global table, so I could check for active mods in data-updates.lua and set a flag, like "global.req_mods_active=true". Then I'd just act on this value in on_load() from control.lua. Great idea -- except in on_load(), you don't have access to global, so global.req_mods_active will always be nil. Unless I overlook something, there is no way to get the list of active mods when loading a normal save.

Doing the check in on_load() does seem necessary, though. Say, you start a new game with only my mod and mod A installed, or you add my mod to a game where only mod A was installed before. In this case, on_init() would trigger and print a reminder to the player's chat console that my mod is useless without B and C. Player ignores this message, save file is overwritten some time later. Next time the game is loaded, my mod is already part of the game, so on_init() won't be called ever again until player changes the configuration or adds/removes mods. With on_load(), I could remind the player every time the game is loaded -- if I could somehow get the information on which mods are installed into the control stage. (Of course, that would also be spamming, but that's another topic!)

Re: Dependencies: Are "optional requirements" possible at all?

Posted: Wed Jun 05, 2019 2:53 pm
by Pi-C
eradicator wrote:
Wed Jun 05, 2019 1:41 pm
Yea. You're in a shitty situation there. More complex depencies don't exist.
Ah, that's what I've been afraid of!
eradicator wrote:
Wed Jun 05, 2019 1:41 pm
I personally don't think throwing error()s works, because "users" don't read error messages. Especially when the text you want to display is sandwiched between a built-in error-header and a stacktrace.
Makes sense.
So your best option is to simply do nothing if the configuration isn't what you need.
That's basically what I do. :-) I started with something like

Code: Select all

if not mods[A] and not mods[B] then OUTPUT MESSAGE else REQUIRE STUFF end
so "doing nothing" would need next to no time at all. After reading that this selective requiring could cause desyncs, I now always require everything and check for active mods before taking any action in the required files. "Doing nothing" takes a bit longer now, but this should be negligible. :-D
And hopefully you don't also have to deal with sitiations where B *and* C are installed?
I didn't so far. If both B and C are present, the item from A will use settings from B only. But thinking more about it, it would make sense to clean up a bit: B has replaced the vanilla item, while C has made an additional item with an additional technology that exists next to the vanilla item. There also is a bigger version for the new item. If B and C are installed, it would make sense to remove the duplicate item and just leave the bigger version in the game.

Re: Dependencies: Are "optional requirements" possible at all?

Posted: Wed Jun 05, 2019 2:54 pm
by Deadlock989
Yeah, reliably printing a message on "game load" (the concept, not the event) is a puzzle. Multiplayer, too. I do much less control scripting than data stage stuff so can't really advise best practice, except that I had more luck with heavily conditional on_player_joined for that kind of thing.

Not to be facetious, but the other obvious solution is: pick a mod. If there are two forks of the same thing in active use, that's not really good news for anyone. One of them is likely to be more worthy of support than the other.

Re: Dependencies: Are "optional requirements" possible at all?

Posted: Wed Jun 05, 2019 3:53 pm
by eradicator
Deadlock989 wrote:
Wed Jun 05, 2019 2:54 pm
Not to be facetious, but the other obvious solution is: pick a mod. If there are two forks of the same thing in active use, that's not really good news for anyone. One of them is likely to be more worthy of support than the other.
That sounds reasonable.
____

I spent more time on control scripting than data, so i can advice on that. First: "global" only exists in control. You can not "transfer" any kind of data between the three stages (settings, data, control) by any means, it's not possible.

What you want is on_init and on_configuration_changed, both of which have game.active_mods. on_config is also called if *another* mod is added, i.e. you can't ever miss any mod changes if you listen to on_config. Your printing in on_init on a brand new map fails because the player does not exist at that point.

Your best bet for nagging messages are on_player_created and on_player_joined_game (mp only). And if you want to be really annoying script.on_nth_tick() with a one hour interval or something.

on_load is not meant for what you're trying to do. the concept of "loading a savegame" does not exist in the gamestate, all mods must behave as if played in one uninterrupted gaming session.

Re: Dependencies: Are "optional requirements" possible at all?

Posted: Wed Jun 05, 2019 4:20 pm
by Pi-C
eradicator wrote:
Wed Jun 05, 2019 3:53 pm
I spent more time on control scripting than data, so i can advice on that. First: "global" only exists in control. You can not "transfer" any kind of data between the three stages (settings, data, control) by any means, it's not possible.
OK.
Your printing in on_init on a brand new map fails because the player does not exist at that point.
Right, I already wondered about that!
Your best bet for nagging messages are on_player_created and on_player_joined_game (mp only).
Are you sure? I'm playing in single player mode only. However, when I created a new game just now, both on_player_created and on_player_joined triggered. I let my character walk around a bit, then saved and reloaded the game. This time, there was no message.
And if you want to be really annoying script.on_nth_tick() with a one hour interval or something.
Nah, that would be too annoying! Still, there's something to it. In order to avoid duplicate messages sent from different events, I've set a flag once the message has been shown, but only show the messages if the flag is unset. Using script.on_nth_tick() with this code, and running it only once per hour, I could make sure that the message is shown at least once per play without nagging the player too much.
on_load is not meant for what you're trying to do. the concept of "loading a savegame" does not exist in the gamestate, all mods must behave as if played in one uninterrupted gaming session.
That would be to avoid issues in multiplayer games?

Re: Dependencies: Are "optional requirements" possible at all?

Posted: Wed Jun 05, 2019 5:05 pm
by eradicator
Pi-C wrote:
Wed Jun 05, 2019 4:20 pm
Your best bet for nagging messages are on_player_created and on_player_joined_game (mp only).
Are you sure? I'm playing in single player mode only. However, when I created a new game just now, both on_player_created and on_player_joined triggered. I let my character walk around a bit, then saved and reloaded the game. This time, there was no message.
You misunderstand what those events do. on_player_created is raised *exactly once* for each player in a game. And then never again. If you reaload the game the player already exists and is not "created". Also this is the LuaPlayer, not your character LuaEntity. on_player_joined is presumably raised when a player "loads" into a running multiplayer game (i don't have much mp modding experience). In singleplayer you never leave, so you can't join more than once.
Pi-C wrote:
Wed Jun 05, 2019 4:20 pm
on_load is not meant for what you're trying to do. the concept of "loading a savegame" does not exist in the gamestate, all mods must behave as if played in one uninterrupted gaming session.
That would be to avoid issues in multiplayer games?
Yes. That is why game does not exist there.

Re: Dependencies: Are "optional requirements" possible at all?

Posted: Wed Jun 05, 2019 5:58 pm
by eduran
A couple of notes on the different startup events and how they behave in multiplayer vs singleplayer:

on_configuration_changed: runs for every mod whenever mod configuration, any mod version, the game version or startup settings change. In MP, it will only ever run for the host/server. Players join afterwards and get a savegame in which on_configuration_changed already ran. Caveat: does not run when a new game is started.

on_player_created: fired when a new player is created. In SP this happens only once when starting the game. In MP it happens whenever a player joins for the first time. Rejoining later does not trigger the event again.

on_player_joined_game: fired whenever a player joins the game. In SP this happens only once. In MP it happens whenever a player (re-)joins the game. One exception is the host, who does not fire a on_player_joined_game event when loading a game. On a headless server, every joining player fires on_player_joined_game, but not the server itself when starting the game.

So, where does that leave you? Imo, this:
Deadlock989 wrote:
Wed Jun 05, 2019 2:54 pm
Not to be facetious, but the other obvious solution is: pick a mod. If there are two forks of the same thing in active use, that's not really good news for anyone. One of them is likely to be more worthy of support than the other.
Alternatively, write a description and hope that most people will read that (yes, yes, I know...). If you really want to do the console message, use both on_player_joined and on_configuration_changed. In MP, it prints the message on every load/join. In SP, it works only once. There is a hacky way to do something on every load in singleplayer, but I fear the wrath of Rseding if I ever share something like that on the forums :D If it's that important to you to remind the player, use on_nth_tick.

Re: Dependencies: Are "optional requirements" possible at all?

Posted: Wed Jun 05, 2019 7:13 pm
by eradicator
eduran wrote:
Wed Jun 05, 2019 5:58 pm
on_player_joined_game: fired whenever a player joins the game. In SP this happens only once. In MP it happens whenever a player (re-)joins the game. One exception is the host, who does not fire a on_player_joined_game event when loading a game. On a headless server, every joining player fires on_player_joined_game, but not the server itself when starting the game.
Uhu. That is interesting. Does that also happen when the host is not game.players[1]? I.e. when you give the savegame to someone else to host it.
eduran wrote:
Wed Jun 05, 2019 5:58 pm
In MP, it prints the message on every load/join.
On second thought i'm not so sure if you should be nagging the players. They can only indirectly influence the servers mods by nagging the host, who might already have forgotten about the message and would have to rely on players to tell him what the message says (uhoh...). So you might want to check "if player.admin then" before nagging someone (You're admin in SP too if you created the map).

Re: Dependencies: Are "optional requirements" possible at all?

Posted: Wed Jun 05, 2019 7:32 pm
by eduran
eradicator wrote:
Wed Jun 05, 2019 7:13 pm
Uhu. That is interesting. Does that also happen when the host is not game.players[1]? I.e. when you give the savegame to someone else to host it.
My statement was not quite accurate. on_player_joined_game is not triggered when the player who saved the game also loads it as the host (same as in single player: that person never left the game). But if someone hosts a game saved by someone else, on_player_joined_game fires.

Re: Dependencies: Are "optional requirements" possible at all?

Posted: Wed Jun 05, 2019 7:50 pm
by eradicator
eduran wrote:
Wed Jun 05, 2019 7:32 pm
eradicator wrote:
Wed Jun 05, 2019 7:13 pm
Uhu. That is interesting. Does that also happen when the host is not game.players[1]? I.e. when you give the savegame to someone else to host it.
My statement was not quite accurate. on_player_joined_game is not triggered when the player who saved the game also loads it as the host (same as in single player: that person never left the game). But if someone hosts a game saved by someone else, on_player_joined_game fires.
That would imply the game remembers for each savegame *who* did the saving. Interesting.

Re: Dependencies: Are "optional requirements" possible at all?

Posted: Thu Jun 06, 2019 1:00 pm
by Pi-C
eradicator wrote:
Wed Jun 05, 2019 5:05 pm
Pi-C wrote:
Wed Jun 05, 2019 4:20 pm
Your best bet for nagging messages are on_player_created and on_player_joined_game (mp only).
Are you sure? I'm playing in single player mode only. However, when I created a new game just now, both on_player_created and on_player_joined triggered. I let my character walk around a bit, then saved and reloaded the game. This time, there was no message.
You misunderstand what those events do. on_player_created is raised *exactly once* for each player in a game. And then never again. If you reaload the game the player already exists and is not "created". Also this is the LuaPlayer, not your character LuaEntity. on_player_joined is presumably raised when a player "loads" into a running multiplayer game (i don't have much mp modding experience). In singleplayer you never leave, so you can't join more than once.
Right, I somehow had this image in my mind where the game was loaded first, and then the player (rather the player's character) was thrown into ("created in") the world. :-)

Re: Dependencies: Are "optional requirements" possible at all?

Posted: Thu Jun 06, 2019 6:12 pm
by Pi-C
eduran wrote:
Wed Jun 05, 2019 5:58 pm
A couple of notes on the different startup events and how they behave in multiplayer vs singleplayer:
Thanks for the detailed information!
eduran wrote:
Wed Jun 05, 2019 5:58 pm
There is a hacky way to do something on every load in singleplayer, but I fear the wrath of Rseding if I ever share something like that on the forums :D If it's that important to you to remind the player, use on_nth_tick.
No need to upset him. I think on_nth_tick is good enough. :-)
eduran wrote:
Wed Jun 05, 2019 5:58 pm
If you really want to do the console message, use both on_player_joined and on_configuration_changed. In MP, it prints the message on every load/join. In SP, it works only once.
Done that.
eradicator wrote:
Wed Jun 05, 2019 7:13 pm
So you might want to check "if player.admin then" before nagging someone (You're admin in SP too if you created the map).
That was a tough one! "player.admin" seemed so obvious, but It took me ages until I came up with these lines:

Code: Select all

script.on_event(defines.events.on_player_joined_game, function(event)
    player = game.get_player(event.player_index)
    if player.admin then
	...
    end
end)
I thought I could just access player.admin (no, player was nil) or use event directly as an index. But I finally figured it out, and it's working now. So, thanks to everybody who has helped me out! :-)

Re: Dependencies: Are "optional requirements" possible at all?

Posted: Thu Jun 06, 2019 6:19 pm
by eradicator
Pi-C wrote:
Thu Jun 06, 2019 6:12 pm

Code: Select all

    player = game.get_player(event.player_index)
Meeep. Meeep. Red Alert! Danger!.

Code: Select all

    local player = game.get_player(event.player_index)
You should use locally scoped variables everywhere. Or else you'll end up with weird hard-to-diagnose bugs when one function reads the global value that another function set. If you don't need the player for anything else you can shorten it to:

Code: Select all

script.on_event(defines.events.on_player_joined_game, function(event)
    if game.get_player(event.player_index).admin then
    
    end
 end)

Re: Dependencies: Are "optional requirements" possible at all?

Posted: Thu Jun 06, 2019 8:08 pm
by Pi-C
eradicator wrote:
Thu Jun 06, 2019 6:19 pm
Pi-C wrote:
Thu Jun 06, 2019 6:12 pm

Code: Select all

    player = game.get_player(event.player_index)
Meeep. Meeep. Red Alert! Danger!.

Code: Select all

    local player = game.get_player(event.player_index)
You should use locally scoped variables everywhere. Or else you'll end up with weird hard-to-diagnose bugs when one function reads the global value that another function set.
Yes, I was too early! Just before I read this, I tried again: The script worked fine for on_player_joined_game, but when on_nth_tick triggered, I got this error:

Code: Select all

attempt to index global 'player' (a nil value)
I made it global so I could use player.print in the external function that prints the nag message. Now I do it this way:

Code: Select all

local function nag(msg, admin)
    if type(admin) == "table" then
        admin.print(msg)
    else
        game.print(msg)
    end
end
on_player_joined_game passes the player table to it, on_nth_tick a boolean value (player is unkown in that event, so I can't print to a specific player and print to all instead).
If you don't need the player for anything else you can shorten it to:
I need to check whether the player is admin, and I need to pass on the table to the function; I think I read somewhere that you should make a local copy of a table if you access it more than once.

By the way, what is cheaper: Passing on the whole player (complete local table) and using player.print in the other function, or reading/passing on the player.player_index and using game.get_player(index).print? My gut feeling tells me I should've used the index … :-)

Re: Dependencies: Are "optional requirements" possible at all?

Posted: Thu Jun 06, 2019 8:42 pm
by eduran
Pi-C wrote:
Thu Jun 06, 2019 8:08 pm

Code: Select all

    if type(admin) == "table" then
You don't have to check the type of admin, just if it was passed to the function:

Code: Select all

    if admin then -- good enough to figure out if the argument was passed
You could also do something like this in on nth-tick:

Code: Select all

for _, player in pairs(game.connected_players) do
  if player.is_admin then
    nag("my msg", player)
  end
end
That way you print to all admins instead of all players and don't need the if check at all.
Pi-C wrote:
Thu Jun 06, 2019 8:08 pm
By the way, what is cheaper: Passing on the whole player (complete local table) and using player.print in the other function, or reading/passing on the player.player_index and using game.get_player(index).print? My gut feeling tells me I should've used the index … :-)
The phrase "complete local table" does not make sense here. Lua does not create a copy of the table, just a new reference. The table itself exists only once. You either pass the reference directly or you pass the index and ask the game to give you a new reference to the same table. Long story short, it makes no noticeable difference. If you already have the player when you call the function, pass it on (saves you one function call). Otherwise do it whichever way you prefer.
Pi-C wrote:
Thu Jun 06, 2019 8:08 pm
I think I read somewhere that you should make a local copy of a table if you access it more than once.
Creating a local reference of functions or tables will speed up code if the you access the table/function multiple times (e.g. inside a loop). But if you call it just a handful of times and/or in events that happen rarely (like the ones we are talking about here), you don't need to worry about that kind of optimization.

Re: Dependencies: Are "optional requirements" possible at all?

Posted: Thu Jun 06, 2019 10:28 pm
by Pi-C
eduran wrote:
Thu Jun 06, 2019 8:42 pm
You could also do something like this in on nth-tick:

Code: Select all

for _, player in pairs(game.connected_players) do
  if player.is_admin then
    nag("my msg", player)
  end
end
That way you print to all admins instead of all players and don't need the if check at all.
Thanks, that's much better! However, I also need to use game.is_multiplayer -- game.connected_players doesn't seem to exist in singleplayer mode.
eduran wrote:
Thu Jun 06, 2019 8:42 pm
Pi-C wrote:
Thu Jun 06, 2019 8:08 pm
By the way, what is cheaper: Passing on the whole player (complete local table) and using player.print in the other function, or reading/passing on the player.player_index and using game.get_player(index).print? My gut feeling tells me I should've used the index … :-)
The phrase "complete local table" does not make sense here. Lua does not create a copy of the table, just a new reference. The table itself exists only once. You either pass the reference directly or you pass the index and ask the game to give you a new reference to the same table. Long story short, it makes no noticeable difference. If you already have the player when you call the function, pass it on (saves you one function call). Otherwise do it whichever way you prefer.
Just a reference? Makes sense …
eduran wrote:
Thu Jun 06, 2019 8:42 pm
Pi-C wrote:
Thu Jun 06, 2019 8:08 pm
I think I read somewhere that you should make a local copy of a table if you access it more than once.
Creating a local reference of functions or tables will speed up code if the you access the table/function multiple times (e.g. inside a loop). But if you call it just a handful of times and/or in events that happen rarely (like the ones we are talking about here), you don't need to worry about that kind of optimization.
Sure, there's not much performance to be gained in this case. But in my experience, it's good to adopt good habits, and to start early -- otherwise one will adopt the bad habit and might see performance drops when one does access the table a lot. :-)

Re: Dependencies: Are "optional requirements" possible at all?

Posted: Fri Jun 07, 2019 5:49 am
by eduran
Pi-C wrote:
Thu Jun 06, 2019 10:28 pm
game.connected_players doesn't seem to exist in singleplayer mode.
When I test it in singleplayer it works and returns a table with one player, myself.