Tutorial:Scripting

From Official Factorio Wiki
Revision as of 09:36, 12 November 2023 by Ickputzdirwech (talk | contribs) (Removed {{Languages}}. added category modding)
Jump to navigation Jump to search

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 global variables you will need, changing game parameters, for instance:

script.on_init(function()
  global.ticker = 0
  global.level = 1
  global.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")
    global.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 global, and setting up conditional event handlers.

local variable
script.on_load(function()
  --Resetting metatables
  for k, v in (global.objects_with_metatable) do
    setmetatable(v, object_metatable)
  end

  --Setting local reference to global variable
  variable = global.variable

  --Conditional event handler
  if global.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 global 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 global 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)
  global.ticker = (global.ticker or 0) + 1
end)

Now when the player saves the game, the value of global.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 global. 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()
  global.story = story_init()
end)

--Register to update the story on events
script.on_event(defines.events, function(event)
  story_update(global.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)