Tutorial:Scripting: Difference between revisions
m (Replaced deprecated source html tag) |
(2.0 update (global -> storage)) |
||
(4 intermediate revisions by 3 users not shown) | |||
Line 1: | Line 1: | ||
{{Languages}} | {{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. | 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 [ | 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. | 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 == | == 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. | 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. | ||
== [https://lua-api.factorio.com/latest/Libraries.html 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.1. | ||
Line 21: | 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 30: | 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() | <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 | ||
Line 51: | Line 47: | ||
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)</syntaxhighlight> | end)</syntaxhighlight> | ||
Line 57: | Line 53: | ||
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 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> | <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 | <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 | ||
Line 108: | Line 104: | ||
that_on_tick(event) | that_on_tick(event) | ||
end)</syntaxhighlight> | 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) | <syntaxhighlight lang="lua">script.on_event(defines.events.on_tick, function(event) | ||
storage.ticker = (storage.ticker or 0) + 1 | |||
end)</syntaxhighlight> | 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 150: | Line 146: | ||
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 158: | Line 154: | ||
<syntaxhighlight lang="lua">--Initialize the story info | <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 273: | Line 269: | ||
--Shorthand syntax for game.surfaces[i], defaults i to 1 | --Shorthand syntax for game.surfaces[i], defaults i to 1 | ||
surface(i)</syntaxhighlight> | 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)