Skip to content
Nicholas Dunn edited this page Jun 4, 2019 · 12 revisions

Welcome to the Factorio Standard Library

What is the Factorio Standard Library

The Factorio Standard Library is a project to bring Factorio modders high-quality, commonly-required utilities and tools that developers have been often copying around, remixing, and rewriting poor quality copies of. Basic tools like logging, math calculations, and position or area manipulation, which involve repeated drudgery and are commonly reproduced in high-quality, documented form in the Factorio Standard Library.

Adding Factorio Stdlib to your mod

  1. Install the Factorio Standard Library mod from the mod portal latest release
  2. In your info.json add stdlib as a dependency.
  3. In your control.lua or other lua modules, you can import the stdlib classes you need.
    • To import a stdlib module, use the lua require syntax.
    • All stdlib files are in the __stdlib__/stdlib/ directory, and then include the rest of the path to the file.
    • For example, to include the Position stdlib module, the file is in stdlib/area/position.lua, so the import would be local Position = require('__stdlib__/stdlib/area/position') (note the lack of file extension)
  4. Stdlib modules need only be included once per file where they are used, but duplicate require statements will not cause issues.

For example, using the logger

For example, using the logger:

-- Require the logger module
local Logger = require('__stdlib__/stdlib/misc/logger'

-- Get an existing Log object or create a new one
Log = Logger('some-file-name')

script.on_event(defines.events.on_built_entity,
    function(event)
        -- Add the created entities name to the log
        Log(event.created_entity.name)
    end
)

In factorio, mod log output will be in %appdata%\factorio\script-output\YourModName (on windows), or ~/.factorio/script-output/YourModName (on OSX or Linux).

By default output is buffered and will only flush everything to the file when a log occurs after the elapsed time (60 seconds)

Avoiding iteration, and general stdlib tools

Often in factorio modding, you have an array of entities and need to apply some action or data to them. For example, if you want to damage or kill biters nearby players when they log in, you might write this code:

script.on_event(defines.events.on_player_created, function(event)
    local player = game.players[event.player_index].character
    local position = player.position
    local force = player.force
    local area = {{position.x - 25, position.y - 25}, {position.x + 25, position.y + 25}}
    local entities = player.surface.find_entities_filtered({area = area, type = 'unit', force = 'enemy'})
    for _, entity in pairs(entities) do
        if entity.valid then
            entity.damage(100, force)
        end
    end
end)

That code is a bit verbose. Particularly creating the area around the player and damaging the entities is very long. It's easy to miss an - or + and make a mistake in the math, especially on long lines.

Improved code

Using the standard library, we can simplify this example. I'm going to use the position and table modules only, and we can shorten the code a bit.

local table = require '__stdlib__/stdlib/utils/table'
local Position = require '__stdlib__/stdlib/area/position'

script.on_event(defines.events.on_player_created, function(event)
    local player = game.players[event.player_index].character
    local force = player.force
    local area = Position(player.position):expand_to_area(25)
    local valid_enemies = table.filter(player.surface.find_entities_filtered({area = area, type = 'unit', force = 'enemy'}), function(e) return e.valid end)
    table.each(valid_enemies, function(entity)
         entity.damage(100, force)
    end)
end)

The second example has a few new require statements at the top, to give access to the extra table methods (table.filter and table.each), and requires the Position class to give access to Position.expand_to_area along with all other position methods.

So, what is the second code doing? Position.expand_to_area creates an area (square) with the player position at the center, and the edges of the area 25 blocks away from each side, just like the previous code did. table.filter takes an array, and is filtering out any that are not true (in this case, are not valid). table.each applies the function to each of the valid enemies, and damages them by 100 HP.

It is easier to understand and read what the code is doing in the second example, and much less error prone. There is no math to get wrong, no verbose iteration.

Improved improved code

However, we aren't done yet. There are a few final improvements that can be done. The function we wrote for table.filter is so common, it is also provided in the stdlib, in Game.VALID_FILTER. We can use that instead of rewriting it ourselves.

Also, we can use the Factorio stdlib event handlers. It allows mods to register multiple handlers for a single event (encouraging smaller, easier-to-read handlers) and has built in error handling. Errors in events can trigger a message in game instead of a hard-exit-to-menu.

Here's the final code!

local table = require('__stdlib__/stdlib/utils/table')
local Game = require('__stdlib__/stdlib/game')
local Event = require('__stdlib__/stdlib/event/event').set_protected_mode(true)
local Position = require('__stdlib__/stdlib/area/position')

Event.register(defines.events.on_player_created, function(event)
    local player = game.get_player(event.player_index)
    local force = player.force
    local area = local area = Position(player.position):expand_to_area(25)
    local valid_enemies = table.filter(player.surface.find_entities_filtered({area = area, type = 'unit', force = 'enemy'}), Game.VALID_FILTER)
    table.each(valid_enemies, function(entity)
         entity.damage(100, force)
    end)
end)

Entity Data

One of the other features the stdlib provides is an easy way to get and set mod data on entities. It is possible to do this yourself, using the global object to store persistent data, but often very cumbersome.

For example, if we wanted a simple mod that kept track of how far a player has driven any particular car, and printed this to the player after they step out (a nicer mod might provide a gui display, but that is a bit outside of the scope), the code would end up quite complex for this basic task:

script.on_event(defines.events.on_player_driving_changed_state, function(event)
    local player = game.players[event.player_index]
    if player.driving then
        local vehicle = player.vehicle
        -- create and keep track of every car
        -- global.cars is an array of tables, ex: { vehicle , start_pos, player_idx }
        if not global.cars then global.cars = {} end

        local data = { vehicle = vehicle, start_pos = player.character.position, player_idx = event.player_index }
        table.insert(global.cars, data)
    else
        if not global.cars then return end

        local found = nil
        for i, car in pairs(global.cars) do
            if car.player_idx == event.player_index then
                 found = car
                 table.remove(global.cars, i)
                 break
            end
        end
        if found then
            local end_pos = player.character.position
            local dist = math.sqrt((end_pos.x - found.start_pos.x) * (end_pos.x - found.start_pos.x) + (end_pos.y - found.start_pos.y) * (end_pos.y - found.start_pos.y))
            player.print("Traveled " .. dist .. " blocks from your starting position!")
        end
    end
end)

There is a lot of complex logic there. Global state has to be tracked, added to when a player enters a vehicle, and removed from when a player exits. The distance traveled from the starting position has to be calculated. Missing variables have to be assigned defaults and edge-cases where a player is in a vehicle before the mod loads have to be handled.

This can be much simpler.

Stdlib version

local Event = require('__stdlib__/stdlib/event/event')
local Entity = require('__stdlib__/stdlib/entity/entity')
local Position = require('__stdlib__/stdlib/area/position')

Event.on_event(defines.events.on_player_driving_changed_state, function(event)
    local player = game.players[event.player_index]
    if player.driving then
        -- track the driving information on the player entity
        Entity.set_data(player.character, { vehicle = vehicle, start_pos = player.character.position, player_idx = event.player_index })
    else
        local data = Entity.get_data(player.character)
        if data then
            player.print("Traveled " .. Position.distance(data.start_pos, player.character.position) .. " blocks from your starting position!")
            -- clear driving info
            Entity.set_data(player.character, nil)
        end
    end
end)

The Factorio Standard Library simplifies much of the code here. The ability to set and retrieve data for an entity removes the need for the for loops, and managing global state ourselves. The ability to easily manipulate positions (and therefore calculate the distance) removes all the mathematical hazards. The mod is now about what actions what needs to happen, and not about complex game state manipulation.

Clone this wiki locally