-
Notifications
You must be signed in to change notification settings - Fork 49
Home
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.
- Install the Factorio Standard Library mod from the mod portal latest release
- In your
info.json
addstdlib
as a dependency. - 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 instdlib/area/position.lua
, so the import would belocal Position = require('__stdlib__/stdlib/area/position')
(note the lack of file extension)
- To import a stdlib module, use the lua
- 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:
-- 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)
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.
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.
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)
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.
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.