diff --git a/.emmyrc.json b/.emmyrc.json
new file mode 100644
index 00000000..2d308c82
--- /dev/null
+++ b/.emmyrc.json
@@ -0,0 +1,48 @@
+{
+ "$schema": "https://raw.githubusercontent.com/EmmyLuaLs/emmylua-analyzer-rust/refs/heads/main/crates/emmylua_code_analysis/resources/schema.json",
+ "runtime": {
+ "version": "Lua5.2",
+ "requirePattern": [ "?", "?.lua" ]
+ },
+ "workspace": {
+ "$comment-moduleMap": "Rewrites each file's own module path rather than the require string",
+ "$comment-library": "The factorio typedefs are provided by a second config, generated with fmtk luals-addon in ci",
+ "$comment-templates": "The create templates contain __plugin_name__ placeholders and are not valid lua",
+ "ignoreGlobs": [
+ "**/node_modules/**",
+ "**/dist/**",
+ "database/**",
+ "docs/**",
+ "external_plugins/**",
+ "factorio/**",
+ "instances/**",
+ "logs/**",
+ "mods/**",
+ "modules/**",
+ "temp/**",
+ "packages/create/templates/**"
+ ],
+ "moduleMap": [
+ { "pattern": "^packages\\.host\\.modules\\.(.+)$", "replace": "modules.$1" },
+ { "pattern": "^plugins\\.([^.]+)\\.module\\.(.+)$", "replace": "modules.$1.$2" },
+ { "pattern": "^packages\\.host\\.lua\\.export\\.(.+)$", "replace": "$1" },
+ { "pattern": "^packages\\.host\\.lua\\.clusterio_lib\\.(.+)$", "replace": "$1" }
+ ]
+ },
+ "doc": {
+ "privateName": [ "__*" ]
+ },
+ "$comment-disable": "Same set as ExpCluster, these are editor level hints rather than ci checks",
+ "$comment-unnecessary-if": "Disabled by the config fmtk generates for itself, the api union types make it unreliable",
+ "diagnostics": {
+ "globals": [ "__DebugAdapter", "__Profiler" ],
+ "disable": [
+ "assign-type-mismatch",
+ "param-type-mismatch",
+ "preferred-local-alias",
+ "unnecessary-assert",
+ "unnecessary-if",
+ "unused"
+ ]
+ }
+}
diff --git a/.github/workflows/lua-lint.yml b/.github/workflows/lua-lint.yml
new file mode 100644
index 00000000..35b51ad2
--- /dev/null
+++ b/.github/workflows/lua-lint.yml
@@ -0,0 +1,83 @@
+name: Lua Lint
+
+on:
+ push:
+ pull_request:
+
+env:
+ EMMYLUA_CHECK_VERSION: "0.24.0"
+
+jobs:
+ lint:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-node@v4
+ with:
+ node-version: lts/*
+ - name: Generate Factorio typedefs
+ run: |
+ npm install --prefix "$RUNNER_TEMP/fmtk" factoriomod-debug
+ "$RUNNER_TEMP/fmtk/node_modules/.bin/fmtk" luals-addon "$RUNNER_TEMP"
+ - name: Install emmylua_check
+ run: |
+ curl -fsSL "https://github.com/EmmyLuaLs/emmylua-analyzer-rust/releases/download/${EMMYLUA_CHECK_VERSION}/emmylua_check-linux-x64.tar.gz" \
+ | tar -xz -C "$RUNNER_TEMP"
+ - name: Run Lint Report
+ shell: bash
+ run: |
+ # The typedefs are generated per run, so their path is passed via a second config
+ jq -n --arg lib "$RUNNER_TEMP/factorio/library" \
+ '{ workspace: { library: [ $lib ] } }' \
+ > "$RUNNER_TEMP/ci.emmyrc.json"
+
+ # Exits non zero on error severity, so the gate below is used instead
+ "$RUNNER_TEMP/emmylua_check" . --config ".emmyrc.json,$RUNNER_TEMP/ci.emmyrc.json" \
+ --output-format json --output check.json || true
+
+ # Credit to https://github.com/Krealle/luals-check-action/blob/main/action.yml
+ # Although some minor fixes were needed
+
+ # Format and print the messages (would be nice to format as a table, but that's too complex for jq)
+ jq -r --arg root "$PWD/" \
+ '.[] | (.file | ltrimstr($root)) as $file | .diagnostics[] |
+ {
+ file: $file,
+ code: .code,
+ line: .range.start.line,
+ message: (.message | split("\n")
+ | map(if startswith("- ") then .[2:] else . end)
+ | join("\n")),
+ severity: (if .severity <= 1 then "ERR" else "WARN" end)
+ } |
+ "**[\(.severity)] \(.code):** \(.file)#L\(.line + 1)
\(.message)"' \
+ check.json > "$GITHUB_STEP_SUMMARY"
+
+ echo ""
+
+ # Github Annotations (limited to 10)
+ jq -r --arg root "$PWD/" \
+ '.[] | (.file | ltrimstr($root)) as $file | .diagnostics[] |
+ {
+ file: $file,
+ title: .code,
+ line: .range.start.line,
+ endLine: .range.end.line,
+ col: .range.start.character,
+ endColumn: .range.end.character,
+ message: (.message | gsub("\n"; "%0A"; "m")),
+ level: (if .severity <= 1 then "error" else "warning" end)
+ } |
+ "::\(.level) file=\(.file),line=\(.line + 1),endLine=\(.endLine + 1),col=\(.col + 1),endColumn=\(.endColumn + 1),title=\(.title)::\(.message) (\(.title))"' \
+ check.json
+
+ # An entry is emitted for every file checked, including clean ones, so
+ # the gate counts diagnostics and asserts the workspace was actually read
+ if [[ $(jq 'length' check.json) -lt 20 ]] ; then
+ echo "::error::Only $(jq 'length' check.json) files were checked, expected the whole workspace"
+ exit 1
+ elif [[ $(jq '[.[].diagnostics[]] | length' check.json) -gt 0 ]] ; then
+ exit 1
+ else
+ echo "✅ All checks succeeded!" | tee "$GITHUB_STEP_SUMMARY"
+ fi
diff --git a/.gitignore b/.gitignore
index 07182d0f..e6710c96 100644
--- a/.gitignore
+++ b/.gitignore
@@ -38,6 +38,7 @@ node_modules
/config-*.json.lock
pnpm-lock.yaml
mod_cache.json
+.luarc.json
/static/
saves
/temp/
diff --git a/packages/host/lua/export/control.lua b/packages/host/lua/export/control.lua
index ae895a8c..402dc1b7 100644
--- a/packages/host/lua/export/control.lua
+++ b/packages/host/lua/export/control.lua
@@ -1,3 +1,6 @@
+--- Undefined fields are expected, the typedefs only cover the latest version
+--- @diagnostic disable: undefined-field
+
local function send_json(channel, data)
data = helpers and helpers.table_to_json(data) or game.table_to_json(data)
print("\f$ipc:" .. channel .. "?j" .. data)
diff --git a/packages/host/modules/clusterio/compat.lua b/packages/host/modules/clusterio/compat.lua
index cafee324..4cd2d7c1 100644
--- a/packages/host/modules/clusterio/compat.lua
+++ b/packages/host/modules/clusterio/compat.lua
@@ -10,7 +10,8 @@ Implementation Guidelines
local lib_json = require("json")
---- @diagnostic disable: deprecated
+--- Undefined fields are expected, the typedefs only cover the latest version
+--- @diagnostic disable: deprecated, undefined-field
--- @class LibCompat
--- @field script_data table
@@ -238,4 +239,6 @@ set_script_data()
compat.on_init = set_script_data --- @package
compat.on_load = set_script_data --- @package
-return setmetatable(compat, compat_mt)
+-- Not returned directly because that would infer the metatable type rather than LibCompat
+setmetatable(compat, compat_mt)
+return compat
diff --git a/packages/host/modules/clusterio/json.lua b/packages/host/modules/clusterio/json.lua
index be01e897..01a01382 100644
--- a/packages/host/modules/clusterio/json.lua
+++ b/packages/host/modules/clusterio/json.lua
@@ -22,6 +22,9 @@
-- SOFTWARE.
--
+--- Vendored file, the forward declared recursion is not worth annotating
+--- @diagnostic disable: need-check-nil
+
local json = { _version = "0.1.2" }
-------------------------------------------------------------------------------
diff --git a/plugins/inventory_sync/module/restore_position.lua b/plugins/inventory_sync/module/restore_position.lua
index f2513133..91da5ea7 100644
--- a/plugins/inventory_sync/module/restore_position.lua
+++ b/plugins/inventory_sync/module/restore_position.lua
@@ -21,9 +21,10 @@ return function(player, record)
player.driving = true
-- Teleport to safe location if unable to enter vehicle
- if not player.driving and player.controller_type == defines.controllers.character then
+ local character = player.character
+ if not player.driving and character and player.controller_type == defines.controllers.character then
local safe_position = record.vehicle.surface.find_non_colliding_position(
- player.character.name, player.position, 32, 1/8
+ character.name, player.position, 32, 1/8
)
if safe_position then
player.teleport(safe_position, player.surface)
diff --git a/plugins/inventory_sync/module/serialize.lua b/plugins/inventory_sync/module/serialize.lua
index a46f6472..9c0aa509 100644
--- a/plugins/inventory_sync/module/serialize.lua
+++ b/plugins/inventory_sync/module/serialize.lua
@@ -207,14 +207,14 @@ end
--- @return QuickBarSlotEncoded
local function serialize_filter(filter)
if filter.quality == "normal" and filter.comparator == "=" then
- return filter.name
+ return filter.name --[[@as string]]
end
return {
t = "f",
- n = filter.name,
- q = filter.quality,
- c = filter.comparator,
+ n = filter.name --[[@as string]],
+ q = filter.quality --[[@as string]],
+ c = filter.comparator --[[@as string]],
}
end
@@ -234,7 +234,7 @@ function serialize.serialize_quick_bar_slot(slot)
if v2_0_quick_bar_api then
-- 2.0 gives us an item filter which supports quality
- --- @cast slot ItemFilter
+ --- @cast slot -QuickBarSlot, -LuaItemPrototype
return serialize_filter(slot)
end
@@ -267,7 +267,7 @@ function serialize.deserialize_quick_bar_slot(entry)
quality = entry.q,
comparator = entry.c,
},
- }
+ } --[[@as QuickBarSlot]]
end
print("Warning: Unsupported serialized quick bar slot type '" .. tostring(entry.t) .. "'")
@@ -292,7 +292,7 @@ function serialize.deserialize_quick_bar_slot(entry)
-- Return a string (member of ItemPrototypeIdentification)
-- Pre 2.0 this was the only method of encoding used
- return entry
+ return entry --[[@as string]]
end
--- @param player LuaPlayer
@@ -709,7 +709,8 @@ local function ensure_character(player)
local surface = player.surface
if v2_space_platform and surface.platform then
local planet_surface = find_planet_surface(surface.platform)
- player.teleport(player.force.get_spawn_position(planet_surface), planet_surface)
+ local force = player.force --[[@as LuaForce]]
+ player.teleport(force.get_spawn_position(planet_surface), planet_surface)
end
-- Create and return the character
@@ -721,7 +722,7 @@ local function ensure_character(player)
player.surface.name, tostring(player.connected), tostring(player.driving)
))
end
- return player.character
+ return assert(player.character)
end
--- @class FailedDeserializationPlayerData
diff --git a/plugins/player_auth/module/auth.lua b/plugins/player_auth/module/auth.lua
index ad195f89..7bbdb9da 100644
--- a/plugins/player_auth/module/auth.lua
+++ b/plugins/player_auth/module/auth.lua
@@ -132,6 +132,7 @@ function auth.add_commands()
end
if event.parameter then
+ --- @type string[]
local args = {}
for w in string.gmatch(event.parameter, "[^ ]+") do
args[#args + 1] = w
@@ -149,7 +150,7 @@ function auth.add_commands()
end
local player_name, url, code = table.unpack(args)
- local player = game.players[player_name]
+ local player = game.get_player(player_name)
if not player then
rcon.print("Player " .. player_name .. " does not exist")
return
@@ -159,7 +160,7 @@ function auth.add_commands()
elseif command == "code_set" then
local player_name = args[1]
- local player = game.players[player_name]
+ local player = game.get_player(player_name)
if not player then
rcon.print("Player " .. player_name .. " does not exist")
return
@@ -171,7 +172,7 @@ function auth.add_commands()
elseif command == "error" then
local player_name = table.remove(args, 1)
- local player = game.players[player_name]
+ local player = game.get_player(player_name)
if not player then
rcon.print("Player " .. player_name .. " does not exist")
return
@@ -192,10 +193,12 @@ auth.events[defines.events.on_gui_click] = function(event)
end
if event.element.name == "player_auth_dialog_close_button" then
- event.element.parent.parent.destroy()
+ local parent = assert(event.element.parent)
+ assert(parent.parent).destroy()
elseif event.element.name == "player_auth_verify_code_button" then
- local verify_code = event.element.parent.player_auth_verify_code_input.text
+ local parent = assert(event.element.parent)
+ local verify_code = assert(parent.player_auth_verify_code_input).text
local player_name = game.players[event.player_index].name
clusterio_api.send_json("player_auth", {
type = "set_verify_code",
diff --git a/plugins/research_sync/module/sync.lua b/plugins/research_sync/module/sync.lua
index 7db1f20d..701f4f7e 100644
--- a/plugins/research_sync/module/sync.lua
+++ b/plugins/research_sync/module/sync.lua
@@ -30,8 +30,17 @@ local function set_technology_progress(tech, progress)
end
end
+--- @class ResearchSyncTechnology
+--- @field level uint
+--- @field researched boolean?
+--- @field progress double?
+
+--- @class ResearchSyncData
+--- @field technologies table
+--- @field ignore_research_finished boolean
+
--- @param no_early_return boolean?
---- @return table
+--- @return ResearchSyncData
local function get_script_data(no_early_return)
local research_sync = compat.script_data.research_sync
if research_sync and not no_early_return then
diff --git a/plugins/statistics_exporter/module/export.lua b/plugins/statistics_exporter/module/export.lua
index 33626000..6a4bb75b 100644
--- a/plugins/statistics_exporter/module/export.lua
+++ b/plugins/statistics_exporter/module/export.lua
@@ -32,6 +32,7 @@ function statistics_exporter.export()
for _, force in pairs(game.forces) do
local flow_statistics = {}
for _, statName in pairs(statistics) do
+ --- @type LuaFlowStatistics
local stat = v2_stats and force["get_" .. statName](surface.index) or force[statName]
flow_statistics[statName] = {
input = stat.input_counts,