Tutorial:Scripting: Difference between revisions
mNo edit summary |
(2.0 update (global -> storage)) |
||
(14 intermediate revisions by 5 users not shown) | |||
Line 1: | Line 1: | ||
{{Languages}} | |||
== What is a script? == | == What is a script? == | ||
In general, you can learn the definition of a script [https://en.wikipedia.org/wiki/Scripting_language here]. Within the context of Factorio, a script refers to a Lua script, packaged with a mod or scenario. | |||
There are 2 usages of scripts, loading of [https://lua-api.factorio.com/latest/index-prototype.html prototype data], and runtime scripting of the game and its entities. | |||
This guide only covers usages of runtime scripts. To be more specific, we call these runtime scripts 'control scripts', as they assert direct control on the game as it runs. | |||
== Loading a script == | |||
When you load a game, scenario and mod scripts are loaded. The game looks for a file called 'control.lua', in the mod directory or the scenario directory. If found, the game will then load that script, and any other scripts required in the control script. | |||
== Factorio specifics == | == [https://lua-api.factorio.com/latest/Libraries.html Factorio specifics] == | ||
Factorio uses Lua version 5.2.1. | |||
Factorio uses Lua version 5.2 | |||
Factorio uses Serpent for serialization - This comes with some big drawbacks: | Factorio uses Serpent for serialization - This comes with some big drawbacks: | ||
Line 16: | Line 18: | ||
* Serpent cannot serialize a lot of Lua objects, such as functions, metatables and coroutines. | * Serpent cannot serialize a lot of Lua objects, such as functions, metatables and coroutines. | ||
The full scripting API is generated and updated each release, and is available [ | The full scripting API is generated and updated each release, and is available [https://lua-api.factorio.com/latest/index-runtime.html here]. This is the number 1 resource for scripting in the game. | ||
== [http://lua-api.factorio.com/latest/LuaBootstrap.html Script events] == | == [http://lua-api.factorio.com/latest/LuaBootstrap.html Script events] == | ||
The Lua script has some special functions it runs outside of events: | The Lua script has some special functions it runs outside of events: | ||
* <code>script.on_init()</code> | * <code>script.on_init()</code> | ||
Line 25: | Line 26: | ||
* <code>script.on_load()</code> | * <code>script.on_load()</code> | ||
<code>on_init()</code> will run when the game starts (or in mod cases, when you add it to an existing save). It is used to initialize | <code>on_init()</code> will run when the game starts (or in mod cases, when you add it to an existing save). It is used to initialize storage variables you will need, changing game parameters, for instance: | ||
< | <syntaxhighlight lang="lua">script.on_init(function() | ||
storage.ticker = 0 | |||
storage.level = 1 | |||
storage.teams = {default_team = "johns-lads"} | |||
game.create_surface("Scenario Surface" | game.create_surface("Scenario Surface") | ||
game.map_settings.pollution.enabled = false | game.map_settings.pollution.enabled = false | ||
--etc. | --etc. | ||
end)</ | end)</syntaxhighlight> | ||
[http://lua-api.factorio.com/latest/LuaGameScript.html <code>game</code>] namespace is available during on_init | [http://lua-api.factorio.com/latest/LuaGameScript.html <code>game</code>] namespace is available during on_init | ||
Line 40: | Line 41: | ||
<code>on_configuration_changed(data)</code> will run when some configuration about this save game changes, such as a mod being added, changed or removed. This function is used to account for changes to game or prototype changes. <code>data</code> will contain information on what has changed. | <code>on_configuration_changed(data)</code> will run when some configuration about this save game changes, such as a mod being added, changed or removed. This function is used to account for changes to game or prototype changes. <code>data</code> will contain information on what has changed. | ||
So if you are | So if you are dependent on some prototype for your script to work, you should check here that it still exists: | ||
< | <syntaxhighlight lang="lua">script.on_configuration_changed(function(data) | ||
local turret_name = "gun-turret" | local turret_name = "gun-turret" | ||
if not game.entity_prototypes[turret_name] then | if not game.entity_prototypes[turret_name] then | ||
log("Gun turret isn't here, some mod or something has changed it") | log("Gun turret isn't here, some mod or something has changed it") | ||
storage.do_turret_logic = false | |||
end | end | ||
end)</ | end)</syntaxhighlight> | ||
The contents of the <code>data</code> parameter is defined [http://lua-api.factorio.com/latest/Concepts.html#ConfigurationChangedData here]. | The contents of the <code>data</code> parameter is defined [http://lua-api.factorio.com/latest/Concepts.html#ConfigurationChangedData here]. | ||
<code>on_load()</code> will run | <code>on_load()</code> will run every time the script loads. <code>game</code> will not be available during <code>on_load</code>. This should only be used to handle resetting up metatables, making local references to variables in <code>storage</code>, and setting up conditional event handlers. | ||
< | <syntaxhighlight lang="lua">local variable | ||
script.on_load(function() | script.on_load(function() | ||
--Resetting metatables | --Resetting metatables | ||
for k, v in ( | for k, v in (storage.objects_with_metatable) do | ||
setmetatable(v, object_metatable) | setmetatable(v, object_metatable) | ||
end | end | ||
--Setting local reference to | --Setting local reference to variable in storage | ||
variable = | variable = storage.variable | ||
--Conditional event handler | --Conditional event handler | ||
if | if storage.trees then | ||
script.on_event(defines.events.on_tick, handle_tree_function) | script.on_event(defines.events.on_tick, handle_tree_function) | ||
end | end | ||
end)</ | end)</syntaxhighlight> | ||
It is quite easy to generate desyncs doing overly complex things with on_load, so we recommend only doing things with it if absolutely nescessary. | It is quite easy to generate desyncs doing overly complex things with on_load, so we recommend only doing things with it if absolutely nescessary. | ||
Line 75: | Line 76: | ||
The scripts all run based off events. These events are sent by the game after certain actions are performed. For instance <code>on_player_crafted_item</code>. To ''do'' something with an event, you will need to assign an event handler: | The scripts all run based off events. These events are sent by the game after certain actions are performed. For instance <code>on_player_crafted_item</code>. To ''do'' something with an event, you will need to assign an event handler: | ||
< | <syntaxhighlight lang="lua">script.on_event(defines.events.on_player_crafted_item, player_crafted_function)</syntaxhighlight> | ||
When the event is triggered, it will then call the function with <code>event</code> as its parameter. <code>event</code> is a table that contains varying information about the event. More specific info on them [http://lua-api.factorio.com/latest/events.html here]. You then process the event using your own function: | When the event is triggered, it will then call the function with <code>event</code> as its parameter. <code>event</code> is a table that contains varying information about the event. More specific info on them [http://lua-api.factorio.com/latest/events.html here]. You then process the event using your own function: | ||
< | <syntaxhighlight lang="lua">function player_crafted_function(event) | ||
game.print("A player crafted an item on tick "..event.tick) | game.print("A player crafted an item on tick "..event.tick) | ||
end</ | end</syntaxhighlight> | ||
This also works with anonymous functions: | This also works with anonymous functions: | ||
< | <syntaxhighlight lang="lua">script.on_event(defines.events.on_tick, function(event) | ||
game.print("tick") | game.print("tick") | ||
end)</ | end)</syntaxhighlight> | ||
Note that only one event handler can be assigned for each event, such that: | Note that only one event handler can be assigned for each event, such that: | ||
< | <syntaxhighlight lang="lua">script.on_event(defines.events.on_tick, function(event) | ||
game.print("tick") | game.print("tick") | ||
end) | end) | ||
Line 94: | Line 95: | ||
script.on_event(defines.events.on_tick, function(event) | script.on_event(defines.events.on_tick, function(event) | ||
game.print("tock") | game.print("tock") | ||
end)</ | end)</syntaxhighlight> | ||
The second handler will overwrite the first. | The second handler will overwrite the first. | ||
If you want to do multiple things on the same event, a simple way is as follows: | If you want to do multiple things on the same event, a simple way is as follows: | ||
< | <syntaxhighlight lang="lua">script.on_event(defines.events.on_tick, function(event) | ||
this_on_tick(event) | this_on_tick(event) | ||
that_on_tick(event) | that_on_tick(event) | ||
end)</ | end)</syntaxhighlight> | ||
== Saving data & the <code> | == Saving data & the <code>storage</code> table == | ||
Save/Load stability is very important to preserve the determinism of the game. In Lua, all values are global by default. This isn’t good news in Factorio, as it can make it seem as though things are working correctly, but will lead to desyncs in MP. | Save/Load stability is very important to preserve the determinism of the game. In Lua, all values are global by default. This isn’t good news in Factorio, as it can make it seem as though things are working correctly, but will lead to desyncs in MP. | ||
To preserve data between load and save, we have the <code> | To preserve data between load and save, we have the <code>storage</code> table. If there is some variable that you need to use between events, this is where it should live. | ||
< | <syntaxhighlight lang="lua">script.on_event(defines.events.on_tick, function(event) | ||
storage.ticker = (storage.ticker or 0) + 1 | |||
end)</ | end)</syntaxhighlight> | ||
Now when the player saves the game, the value of <code> | Now when the player saves the game, the value of <code>storage.ticker</code> will be saved. When it is loaded again, its value will be restored. This is important, because in MP, players joining the game will load the value from the save. | ||
The way this can cause desyncs is quite clear, if one player has their Lua state, with a ticker value of 100, and another has a value of 50, and you then create ticker number of biters, it would create 100 for player 1, and 50 for player 2. | The way this can cause desyncs is quite clear, if one player has their Lua state, with a ticker value of 100, and another has a value of 50, and you then create ticker number of biters, it would create 100 for player 1, and 50 for player 2. | ||
Line 118: | Line 119: | ||
However it is not often this easy. The case is often as follows: | However it is not often this easy. The case is often as follows: | ||
< | <syntaxhighlight lang="lua">tick_to_print = 10 --So lets say this is a static variable. Just some config for how your script works. | ||
function on_tick() | function on_tick() | ||
Line 126: | Line 127: | ||
end | end | ||
script.on_event(defines.event.on_tick, on_tick)</ | script.on_event(defines.event.on_tick, on_tick)</syntaxhighlight> | ||
This will be fine, we are using the variable, but not changing it. When a new player loads the game, <code>tick_to_print</code> will be <code>10</code>, same as the other players. | This will be fine, we are using the variable, but not changing it. When a new player loads the game, <code>tick_to_print</code> will be <code>10</code>, same as the other players. | ||
Problem comes if we adjust <code>tick_to_print</code>, either by purpose or intentionally. | Problem comes if we adjust <code>tick_to_print</code>, either by purpose or intentionally. | ||
< | <syntaxhighlight lang="lua">tick_to_print = 10 --So lets say this is a static variable. Just some config for how your script works. | ||
function on_tick() | function on_tick() | ||
Line 140: | Line 141: | ||
end | end | ||
script.on_event(defines.event.on_tick, on_tick)</ | script.on_event(defines.event.on_tick, on_tick)</syntaxhighlight> | ||
Now when you test the script, it will work perfectly. Every 100 ticks it will print “hello”. The problem occurs when you save, and load the game. When you save, the value of <code>tick_to_print</code> is not saved anywhere, And thus, when you load, it just uses the value its told to at the top of the script: <code>10</code>. | Now when you test the script, it will work perfectly. Every 100 ticks it will print “hello”. The problem occurs when you save, and load the game. When you save, the value of <code>tick_to_print</code> is not saved anywhere, And thus, when you load, it just uses the value its told to at the top of the script: <code>10</code>. | ||
Now it is clear that the value of <code>tick_to_print</code> has changed by saving and loading, thus we say it is not save/load stable. If a players joins a MP game with this script, they would desync as soon as one of the scripts prints as the result of a comparison against <code>tick_to_print</code>. | Now it is clear that the value of <code>tick_to_print</code> has changed by saving and loading, thus we say it is not save/load stable. If a players joins a MP game with this script, they would desync as soon as one of the scripts prints as the result of a comparison against <code>tick_to_print</code>. | ||
So TL;DR - If you want to use something between ticks, then store it in <code> | So TL;DR - If you want to use something between ticks, then store it in <code>storage</code>. This will prevent 99% of possible desyncs. | ||
== Story script == | == Story script == | ||
Line 151: | Line 152: | ||
Story script is a Lua library designed to facilitate the scripting and flow of a story. It has some simple structure and supporting function to help things move along. | Story script is a Lua library designed to facilitate the scripting and flow of a story. It has some simple structure and supporting function to help things move along. | ||
< | <syntaxhighlight lang="lua">--Initialize the story info | ||
script.on_init(function() | script.on_init(function() | ||
storage.story = story_init() | |||
end) | end) | ||
--Register to update the story on events | --Register to update the story on events | ||
script.on_event(defines.events, function(event) | script.on_event(defines.events, function(event) | ||
story_update( | story_update(storage.story, event) | ||
end) | end) | ||
Line 212: | Line 213: | ||
--Init the helpers and story table. Must be done all times script is loaded. | --Init the helpers and story table. Must be done all times script is loaded. | ||
story_init_helpers(story_table)</ | story_init_helpers(story_table)</syntaxhighlight> | ||
Branches are an optional system of the story system, and you can jump to another branch using <code>story_jump_to(story, name)</code>. The story progresses from the top down, and when it reaches the last story event, the mission concludes. | Branches are an optional system of the story system, and you can jump to another branch using <code>story_jump_to(story, name)</code>. The story progresses from the top down, and when it reaches the last story event, the mission concludes. | ||
Line 220: | Line 221: | ||
Bad: | Bad: | ||
< | <syntaxhighlight lang="lua"> { | ||
action = function() | action = function() | ||
player().insert("iron-plate") | player().insert("iron-plate") | ||
Line 229: | Line 230: | ||
player().print("Use your iron plate to craft some belts") | player().print("Use your iron plate to craft some belts") | ||
end | end | ||
}</ | }</syntaxhighlight> | ||
Good: | Good: | ||
< | <syntaxhighlight lang="lua"> { | ||
init = function() | init = function() | ||
player().insert("iron-plate") | player().insert("iron-plate") | ||
player().print("Use your iron plate...") | player().print("Use your iron plate...") | ||
end | end | ||
}</ | }</syntaxhighlight> | ||
There are some utility functions to make things simpler on the scripting side: | There are some utility functions to make things simpler on the scripting side: | ||
< | <syntaxhighlight lang="lua">--Return true only after this many seconds has elasped | ||
story_elapsed_check(5) | story_elapsed_check(5) | ||
Line 267: | Line 268: | ||
--Shorthand syntax for game.surfaces[i], defaults i to 1 | --Shorthand syntax for game.surfaces[i], defaults i to 1 | ||
surface(i)</ | surface(i)</syntaxhighlight> | ||
[[Category:Modding]] |
Latest revision as of 16:59, 13 November 2024
What is a script?
In general, you can learn the definition of a script here. Within the context of Factorio, a script refers to a Lua script, packaged with a mod or scenario.
There are 2 usages of scripts, loading of prototype data, and runtime scripting of the game and its entities.
This guide only covers usages of runtime scripts. To be more specific, we call these runtime scripts 'control scripts', as they assert direct control on the game as it runs.
Loading a script
When you load a game, scenario and mod scripts are loaded. The game looks for a file called 'control.lua', in the mod directory or the scenario directory. If found, the game will then load that script, and any other scripts required in the control script.
Factorio specifics
Factorio uses Lua version 5.2.1.
Factorio uses Serpent for serialization - This comes with some big drawbacks:
- Serpent is relatively slow and inefficient
- Serpent cannot serialize a lot of Lua objects, such as functions, metatables and coroutines.
The full scripting API is generated and updated each release, and is available here. This is the number 1 resource for scripting in the game.
Script events
The Lua script has some special functions it runs outside of events:
script.on_init()
script.on_configuration_changed()
script.on_load()
on_init()
will run when the game starts (or in mod cases, when you add it to an existing save). It is used to initialize storage variables you will need, changing game parameters, for instance:
script.on_init(function()
storage.ticker = 0
storage.level = 1
storage.teams = {default_team = "johns-lads"}
game.create_surface("Scenario Surface")
game.map_settings.pollution.enabled = false
--etc.
end)
game
namespace is available during on_init
on_configuration_changed(data)
will run when some configuration about this save game changes, such as a mod being added, changed or removed. This function is used to account for changes to game or prototype changes. data
will contain information on what has changed.
So if you are dependent on some prototype for your script to work, you should check here that it still exists:
script.on_configuration_changed(function(data)
local turret_name = "gun-turret"
if not game.entity_prototypes[turret_name] then
log("Gun turret isn't here, some mod or something has changed it")
storage.do_turret_logic = false
end
end)
The contents of the data
parameter is defined here.
on_load()
will run every time the script loads. game
will not be available during on_load
. This should only be used to handle resetting up metatables, making local references to variables in storage
, and setting up conditional event handlers.
local variable
script.on_load(function()
--Resetting metatables
for k, v in (storage.objects_with_metatable) do
setmetatable(v, object_metatable)
end
--Setting local reference to variable in storage
variable = storage.variable
--Conditional event handler
if storage.trees then
script.on_event(defines.events.on_tick, handle_tree_function)
end
end)
It is quite easy to generate desyncs doing overly complex things with on_load, so we recommend only doing things with it if absolutely nescessary.
Game events
The scripts all run based off events. These events are sent by the game after certain actions are performed. For instance on_player_crafted_item
. To do something with an event, you will need to assign an event handler:
script.on_event(defines.events.on_player_crafted_item, player_crafted_function)
When the event is triggered, it will then call the function with event
as its parameter. event
is a table that contains varying information about the event. More specific info on them here. You then process the event using your own function:
function player_crafted_function(event)
game.print("A player crafted an item on tick "..event.tick)
end
This also works with anonymous functions:
script.on_event(defines.events.on_tick, function(event)
game.print("tick")
end)
Note that only one event handler can be assigned for each event, such that:
script.on_event(defines.events.on_tick, function(event)
game.print("tick")
end)
script.on_event(defines.events.on_tick, function(event)
game.print("tock")
end)
The second handler will overwrite the first.
If you want to do multiple things on the same event, a simple way is as follows:
script.on_event(defines.events.on_tick, function(event)
this_on_tick(event)
that_on_tick(event)
end)
Saving data & the storage
table
Save/Load stability is very important to preserve the determinism of the game. In Lua, all values are global by default. This isn’t good news in Factorio, as it can make it seem as though things are working correctly, but will lead to desyncs in MP.
To preserve data between load and save, we have the storage
table. If there is some variable that you need to use between events, this is where it should live.
script.on_event(defines.events.on_tick, function(event)
storage.ticker = (storage.ticker or 0) + 1
end)
Now when the player saves the game, the value of storage.ticker
will be saved. When it is loaded again, its value will be restored. This is important, because in MP, players joining the game will load the value from the save.
The way this can cause desyncs is quite clear, if one player has their Lua state, with a ticker value of 100, and another has a value of 50, and you then create ticker number of biters, it would create 100 for player 1, and 50 for player 2.
However it is not often this easy. The case is often as follows:
tick_to_print = 10 --So lets say this is a static variable. Just some config for how your script works.
function on_tick()
if game.tick == tick_to_print then
game.print("hello")
end
end
script.on_event(defines.event.on_tick, on_tick)
This will be fine, we are using the variable, but not changing it. When a new player loads the game, tick_to_print
will be 10
, same as the other players.
Problem comes if we adjust tick_to_print
, either by purpose or intentionally.
tick_to_print = 10 --So lets say this is a static variable. Just some config for how your script works.
function on_tick()
if game.tick == tick_to_print then
game.print("hello")
tick_to_print = tick_to_print + 100 --Say we want it to print again in 100 ticks
end
end
script.on_event(defines.event.on_tick, on_tick)
Now when you test the script, it will work perfectly. Every 100 ticks it will print “hello”. The problem occurs when you save, and load the game. When you save, the value of tick_to_print
is not saved anywhere, And thus, when you load, it just uses the value its told to at the top of the script: 10
.
Now it is clear that the value of tick_to_print
has changed by saving and loading, thus we say it is not save/load stable. If a players joins a MP game with this script, they would desync as soon as one of the scripts prints as the result of a comparison against tick_to_print
.
So TL;DR - If you want to use something between ticks, then store it in storage
. This will prevent 99% of possible desyncs.
Story script
Story script is a Lua library designed to facilitate the scripting and flow of a story. It has some simple structure and supporting function to help things move along.
--Initialize the story info
script.on_init(function()
storage.story = story_init()
end)
--Register to update the story on events
script.on_event(defines.events, function(event)
story_update(storage.story, event)
end)
--Story table is where the 'story' is all defined.
story_table =
{
{
--branch 1
{
--First story event
--Initialise this event
init = function(event, story)
game.print("First init of first story event")
end,
--Update function that will run on all events
update = function(event, story)
log("updating")
end,
--Condition to move on. If the return value is 'true', the story will continue.
condition = function(event, story)
if event.tick > 100 then
return true
end
end,
--Action to perform after condition is met
action = function(event, story)
game.print("You completed the objective!")
end
},
{
--Second story event - example.
init = function(event, story)
game.print("Collect 100 iron plate")
end,
condition = function(event, story)
return game.players[1].get_item_count("iron-plate") >= 100
end,
action = function(event, story)
game.print("Well done")
end
}
--Once the end of a branch is reached, the story is finished.
--The game will now display the mission complete screen.
},
{
--branch 2
}
}
--Init the helpers and story table. Must be done all times script is loaded.
story_init_helpers(story_table)
Branches are an optional system of the story system, and you can jump to another branch using story_jump_to(story, name)
. The story progresses from the top down, and when it reaches the last story event, the mission concludes.
It is possible to leverage any number of clever Lua tricks and API calls in the story table. It is good form to try and keep each story part independant from its neighbors, as it makes maintanance and reworkings more manageable.
Bad:
{
action = function()
player().insert("iron-plate")
end
},
{
init = function()
player().print("Use your iron plate to craft some belts")
end
}
Good:
{
init = function()
player().insert("iron-plate")
player().print("Use your iron plate...")
end
}
There are some utility functions to make things simpler on the scripting side:
--Return true only after this many seconds has elasped
story_elapsed_check(5)
--Update the objective for all players
set_goal({"objective-1"})
--Flash the goal GUI for players (updating info doesn't flash it)
flash_goal()
--Update the 'info gui' for all players. This is quite powerful and can use custom function to build complex GUI's
set_info(
{
text = {"info-1"},
picture = "item/iron-plate"
})
--Exports entities in a Lua table format
export_entities(parameters)
--Recreates entities saved using a Lua format
recreate_entities(entities, parameters)
--Shorthand syntax for game.players[i], defaults i to 1
player(i)
--Shorthand syntax for game.surfaces[i], defaults i to 1
surface(i)