Files
factorio-expcluster/exp_util

ExpGaming - Module Utilities

Lua library the other ExpCluster plugins build on. It adds to the math and table globals, guards require at runtime, and has modules for storage, tasks spread over ticks, selection tools, flying text and bounding boxes. It has no config, and the Node side is empty so the module gets patched into the save.

Loading the module adds math.round, math.clamp, table.get_keys, table.inspect, table.deep_copy and more to the globals. package.lifecycle tracks the current stage. Once the first player joins, or on singleplayer or multiplayer init, the stage is runtime. From then on require only returns files that were already loaded and raises an error for anything else. Functions marked control stage only below raise an error at runtime.

Lua

local ExpUtil = require("modules/exp_util")
local Storage = require("modules/exp_util/storage")
local Async = require("modules/exp_util/async")
local Selection = require("modules/exp_util/selection")
local FlyingText = require("modules/exp_util/flying_text")
local AABB = require("modules/exp_util/aabb")
local Colors = require("modules/exp_util/include/color") -- Also ExpUtil.color
local FlowPrecision = require("modules/exp_util/include/flow_precision")

Storage gives each file its own table in storage.exp_storage, keyed by the file path. Register once per file, in the control stage:

local data = { players = {} }
Storage.register(data, function(tbl) data = tbl end) -- Called on init and load
Storage.register_metatable("MyClass", metatable) -- Prefixed with the module name

Async runs a function on a later tick. Its calls can be kept in storage. Register in the control stage. The id is the file and line the function is defined on, so moving it breaks calls queued in an existing save.

local say = Async.register(function(name) game.print("Hello " .. name) end)
say("Dave") -- Next tick, same as say:start_soon("Dave")
say:start_after(60, "Dave")
say:start_now("Dave") -- This tick
say:start_task("Dave") -- Only if no call to it is queued
local pending = say("Dave") -- pending:cancel() or pending:return_to(other_async)

return Async.status.continue(...) -- From inside, run again next tick with new arguments
return Async.status.delay(ticks, ...)
return Async.status.complete(...) -- Or return nothing

Selection gives a player a selection-tool and sends the area events to the active selection. It stops when the tool leaves the cursor, or the player leaves or dies.

local MySelection = Selection.connect("my_selection") -- Control stage only
MySelection:on_selection(function(event, ...) end) -- Also on_alt_selection, on_start, on_stop
MySelection:start(player, ...) -- Extra arguments are passed to the handlers
MySelection:stop(player)

Other functions used often. transfer_inventory_to_surface moves the items into matching entities on the surface and creates more when allow_creation is set.

ExpUtil.assert_not_runtime()
ExpUtil.assert_argument_type(value, "table", 1, "name")
ExpUtil.write_json(path, tbl) -- Appends one JSON line to script-output on the server
ExpUtil.optional_require(path) -- nil if the file does not exist
ExpUtil.auto_complete(options, input) -- Best match, prefers matches at the start of a word
ExpUtil.format_any(value) -- Readable string or locale string for any value
ExpUtil.format_time(ticks, "short", { hours = true, minutes = true }) -- Also "long" and "clock"
ExpUtil.format_player_name(player) -- Name in the player's chat color
ExpUtil.teleport_player(player, surface, position, "dismount") -- Or "allow", "disallow"
ExpUtil.transfer_inventory_to_surface{ inventory = inv, surface = surface, name = "iron-chest", allow_creation = true }
FlyingText.create_above_player{ target_player = player, text = "Hi" }
AABB.contains_point(aabb, point)