Commands, Permission Groups, Player Statistics, Reports, Roles, Scenario, Server UPS and Module Utilities get a readme covering their config, permissions, commands, web pages and Lua and Node API. The existing readmes are brought up to date: Auto Restart now that clusterio marks a restart after a plugin install, Discord Auth gets the code timeout, command aliases and its Lua API, and the GUI guide says what the plugin is and has its toolbar examples and an iterator name fixed. The main readme gets a Plugins section listing each plugin by name and package with a one line description. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
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)