Files
factorio-clusterio/packages/host/modules/clusterio/compat.lua
bbassieandClaude Fable 5 a25f8c8611 Fix lua lint warnings reported by emmylua
The largest source was compat.lua returning the setmetatable call
directly, which made emmylua infer the module type from the metatable
rather than the LibCompat class, hiding every field from consumers.

The remaining fixes are nil guards on gui element and character
accesses, type annotations for research_sync's script data and
player_auth's rcon arguments, casts where the api's read/write union
types are wider than the values at runtime, and scoped diagnostic
disables for the vendored json.lua and the version fallback code that
touches apis removed from the current typedefs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-27 22:25:24 +00:00

245 lines
7.2 KiB
Lua

--[[
Adds various functions used to ensure compatibly between version including some polyfills.
Implementation Guidelines
- If a function is only available at runtime in one version, then all versions must be made to error.
- Where possible, directly assign the function / table to the member of compat.
- When not possible, then it should replace itself with a direct assignment asap.
- Versions are handled in ascending order covering all supported versions and ending with an error.
]]
local lib_json = require("json")
--- Undefined fields are expected, the typedefs only cover the latest version
--- @diagnostic disable: deprecated, undefined-field
--- @class LibCompat
--- @field script_data table
--- @field prototypes LuaPrototypes
--- @field active_mods table<string, string>
--- @field write_file fun(filename: string, data: LocalisedString, append: boolean?, for_player: uint?)
--- @field encode_string fun(string: string): string
--- @field decode_string fun(string: string): string
--- @field table_to_json fun(data: table): string
--- @field json_to_table fun(json: string): AnyBasic?
local compat = {}
--- @class LibCompatMt
--- @package
local compat_mt = {}
--- @alias VersionTable { [1]: number, [2]: number, [3]: number }
--- Concert a version string into a version table
--- @param version string
--- @return VersionTable
local function version_to_table(version)
local index = 0
local rtn = { 0, 0, 0 }
for part in version:gmatch("%d+") do
index = index + 1
rtn[index] = tonumber(part)
end
return rtn
end
--- The current version of factorio, the earliest supported is 0.17.69
local current_version_str = "0.17.69"
local current_version = version_to_table(current_version_str)
do
local success, base_version = pcall(function()
return script.active_mods.base
end)
if success then
current_version_str = base_version
current_version = version_to_table(current_version_str)
end
end
--- Returns true if the current version is equal to the provided version
--- @param version string Version to compare against
--- @return boolean # True if versions are equal
function compat.version_eq(version)
local version_tbl = version_to_table(version)
for i = 1, 3 do
if version_tbl[i] ~= current_version[i] then
return false
end
end
return true
end
--- Returns true if the current version is greater than or equal to the the provided version
--- @param version string Version to compare against
--- @return boolean # True if the version is greater or equal to provided
function compat.version_ge(version)
local version_tbl = version_to_table(version)
for i = 1, 3 do
if version_tbl[i] > current_version[i] then
return false
elseif version_tbl[i] < current_version[i] then
return true
end
end
return true
end
--- Returns true if the current version is less than or equal to the the provided version
--- @param version string Version to compare against
--- @return boolean # True if the version is less or equal to provided
function compat.version_le(version)
local version_tbl = version_to_table(version)
for i = 1, 3 do
if version_tbl[i] < current_version[i] then
return false
elseif version_tbl[i] > current_version[i] then
return true
end
end
return true
end
--- The major versions of factorio we support
local v2 = compat.version_ge("2.0")
local v1 = compat.version_ge("1.0") and not compat.version_ge("2.0")
local v0 = compat.version_ge("0.17") and not compat.version_ge("1.0")
--- The minor versions of factorio we support
local v2_0 = compat.version_ge("2.0")
local v1_1 = compat.version_ge("1.1") and not compat.version_ge("2.0")
local v1_0 = compat.version_ge("1.0") and not compat.version_ge("1.1")
local v0_18 = compat.version_ge("0.18") and not compat.version_ge("1.0")
local v0_17 = compat.version_ge("0.17") and not compat.version_ge("0.18")
--- Raise an error because the current version is unsupported
local function unsupported_version()
error("Unsupported version: " .. current_version_str, 3)
end
--- Raise an error because the function is only available at runtime
local function runtime_only()
error("To maintain compatibly, this value can only be used during runtime.", 3)
end
--- Keys of this table will raise an unsupported error when accessed
local unsupported_properties = {}
--- Keys of this table can only be accessed at runtime
--- The value is a function which returns the real value
local runtime_properties = {}
--- Handles the indexing of the tables above
function compat_mt:__index(key)
if unsupported_properties[key] then
return unsupported_version()
elseif runtime_properties[key] then
if game == nil then
return runtime_only()
end
local value = runtime_properties[key]()
if value == nil then
return unsupported_version()
end
rawset(self, key, value)
return value
end
end
--- Prototype data contains all the definitions for the game, it is only available during runtime
function runtime_properties.prototypes()
if v0 or v1 then
--- Lazy loading of prototype data
--- Iteration is UB, we can fix this once someone actually needs it
return setmetatable({}, {
__index = function(self, key)
local value = game[key .. "_prototypes"]
rawset(self, key, value)
return value
end
})
elseif v2 then
return prototypes
end
end
--- List of all active mods
function runtime_properties.active_mods()
if v0_17 then
return game.active_mods
elseif v0 or v1 or v2 then
return script.active_mods
end
end
--- Writes a string to file
function runtime_properties.write_file()
if v0 or v1 then
return game.write_file
elseif v2 then
return helpers.write_file
end
end
--- Deflates and base64 encodes the given string.
function runtime_properties.encode_string()
if (v0_18 and compat.version_ge("0.18.7")) or v1 then
return game.encode_string
elseif v2 then
return helpers.encode_string
end
end
--- Base64 decodes and inflates the given string.
function runtime_properties.decode_string()
if (v0_18 and compat.version_ge("0.18.7")) or v1 then
return game.decode_string
elseif v2 then
return helpers.decode_string
end
end
--- Convert tables to and from json, we have backported this to work before runtime
if v0 or v1 then
compat.table_to_json = function(tbl)
if game then
compat.table_to_json = game.table_to_json
return game.table_to_json(tbl)
end
return lib_json.encode(tbl)
end
compat.json_to_table = function(json)
if game then
compat.json_to_table = game.json_to_table
return game.json_to_table(json)
end
return lib_json.decode(json)
end
elseif v2 then
compat.table_to_json = helpers.table_to_json
compat.json_to_table = helpers.json_to_table
else
unsupported_properties.table_to_json = true
unsupported_properties.json_to_table = true
end
--- Script data refers to lua data which is persisted between loads
--- This is called immediately, during init and during load
local function set_script_data()
if v0 or v1 then
--- @diagnostic disable-next-line
compat.script_data = global
elseif v2 then
compat.script_data = storage
else
unsupported_properties.script_data = true
end
end
set_script_data()
compat.on_init = set_script_data --- @package
compat.on_load = set_script_data --- @package
-- Not returned directly because that would infer the metatable type rather than LibCompat
setmetatable(compat, compat_mt)
return compat