diff --git a/.luarc.json b/.luarc.json index 2e32d2b1..b22e649b 100644 --- a/.luarc.json +++ b/.luarc.json @@ -1,12 +1,13 @@ { "$schema": "https://raw.githubusercontent.com/LuaLS/vscode-lua/master/setting/schema.json", "completion.requireSeparator": "/", - "doc.privateName": [ "__([_%w]+)" ], - "doc.packageName": [ "_([_%w]+)" ], + "doc.regengine": "glob", + "doc.privateName": [ "__*" ], + "doc.packageName": [ "_*" ], "runtime.pluginArgs": [ "--clusterio-modules" ], - "diagnostics.unusedLocalExclude": [ "_([_%w]*)", "i", "j", "k", "v" ], + "diagnostics.unusedLocalExclude": [ "_*", "i", "j", "k", "v" ], "diagnostics.groupFileStatus": { "ambiguity": "Any", "await": "None", @@ -32,49 +33,49 @@ "nameStyle.config": { "local_name_style": [{ "type" : "pattern", - "param": "_?_?(%w+)?", + "param": "_?_?(\\S*)", "$1": "snake_case" }], "module_local_name_style": [{ "type" : "pattern", - "param": "_?_?(\\w+)", + "param": "_?_?(\\S*)", "$1": "snake_case" }, { "type" : "pattern", - "param": "_?_?(\\w+)", + "param": "_?_?(\\S*)", "$1": "upper_snake_case" }, { "type" : "pattern", - "param": "_?_?(\\w+)", + "param": "_?_?(\\S*)", "$1": "pascal_case" }], "function_param_name_style": [{ "type" : "pattern", - "param": "_?_?(\\w+)?", + "param": "_?_?(\\S*)", "$1": "snake_case" }], "function_name_style": [{ "type" : "pattern", - "param": "_?_?(\\w+)", + "param": "_?_?(\\S*)", "$1": "snake_case" }], "local_function_name_style": [{ "type" : "pattern", - "param": "_?_?(\\w+)", + "param": "_?_?(\\S*)", "$1": "snake_case" }], "table_field_name_style": [{ "type" : "pattern", - "param": "_?_?(\\w+)", + "param": "_?_?(\\S*)", "$1": "snake_case" }], "global_variable_name_style": [{ "type" : "pattern", - "param": "_?_?(\\w+)", + "param": "_?_?(\\S*)", "$1": "snake_case" }, { "type" : "pattern", - "param": "_?_?(\\w+)", + "param": "_?_?(\\S*)", "$1": "upper_snake_case" }], "module_name_style": [ "pascal_case", "snake_case" ], diff --git a/exp_util/module/aabb.lua b/exp_util/module/aabb.lua new file mode 100644 index 00000000..dda22d75 --- /dev/null +++ b/exp_util/module/aabb.lua @@ -0,0 +1,100 @@ +--[[-- ExpUtil - AABB +Provides a set of common functions for working with axis aligned bounding boxes +]] + +local floor = math.floor +local ceil = math.ceil +local min = math.min +local max = math.max + +--- @class ExpUtil_AABB +local AABB = {} + +--- Check if an area is valid +--- @param aabb BoundingBox +--- @return boolean # True if the area is valid +function AABB.valid(aabb) + return aabb.left_top.x < aabb.right_bottom.x and aabb.left_top.y < aabb.right_bottom.y +end + +--- Clone an area, allows for safe mutation of an input value +--- @param aabb BoundingBox +--- @return BoundingBox +function AABB.clone(aabb) + return { + left_top = { x = aabb.left_top.x, y = aabb.left_top.y }, + right_bottom = { x = aabb.right_bottom.x, y = aabb.right_bottom.y }, + } +end + +--- Expand an area to be integer aligned, expanding away from 0 +--- @param aabb BoundingBox +--- @return BoundingBox +function AABB.expand(aabb) + return { + left_top = { x = floor(aabb.left_top.x), y = floor(aabb.left_top.y) }, + right_bottom = { x = ceil(aabb.right_bottom.x), y = ceil(aabb.right_bottom.y) }, + } +end + +--- Contract an area to be integer aligned, contracting towards 0 +--- @param aabb BoundingBox +--- @return BoundingBox +function AABB.contract(aabb) + return { + left_top = { x = ceil(aabb.left_top.x), y = ceil(aabb.left_top.y) }, + right_bottom = { x = floor(aabb.right_bottom.x), y = floor(aabb.right_bottom.y) }, + } +end + +--- Expand an area to include all other areas +--- @param aabb BoundingBox +--- @param ... BoundingBox +--- @return BoundingBox +function AABB.union(aabb, ...) + local rtn = AABB.clone(aabb) + for _, next_aabb in ipairs{ ... } do + rtn.left_top.x = min(rtn.left_top.x, next_aabb.left_top.x) + rtn.left_top.y = min(rtn.left_top.y, next_aabb.left_top.y) + rtn.right_bottom.x = max(rtn.right_bottom.x, next_aabb.right_bottom.x) + rtn.right_bottom.y = max(rtn.right_bottom.y, next_aabb.right_bottom.y) + end + return rtn +end + +--- Contract an area to include to the overlap of all areas +--- @param aabb BoundingBox +--- @param ... BoundingBox +--- @return BoundingBox? # Nil if there is no intersection +function AABB.intersect(aabb, ...) + local rtn = AABB.clone(aabb) + for _, next_aabb in ipairs{ ... } do + rtn.left_top.x = max(rtn.left_top.x, next_aabb.left_top.x) + rtn.left_top.y = max(rtn.left_top.y, next_aabb.left_top.y) + rtn.right_bottom.x = min(rtn.right_bottom.x, next_aabb.right_bottom.x) + rtn.right_bottom.y = min(rtn.right_bottom.y, next_aabb.right_bottom.y) + if not AABB.valid(rtn) then + return nil + end + end + return rtn +end + +--- Check if a point is contained within an area +--- @param aabb BoundingBox +--- @param point MapPosition +--- @return boolean # True if the point is within or on the edge of the bounding box +function AABB.contains_point(aabb, point) + return point.x >= aabb.left_top.x and point.y >= aabb.left_top.y + and point.x <= aabb.right_bottom.x and point.y <= aabb.right_bottom.y +end + +--- Check if an area is fulling contained within another area +--- @param aabb BoundingBox +--- @param other BoundingBox +--- @return boolean # True if the point is within or on the edge of the bounding box +function AABB.contains_area(aabb, other) + return AABB.contains_point(aabb, other.left_top) and AABB.contains_point(aabb, other.right_bottom) +end + +return AABB diff --git a/exp_util/module/async.lua b/exp_util/module/async.lua index fd77ed52..ba37af7f 100644 --- a/exp_util/module/async.lua +++ b/exp_util/module/async.lua @@ -81,18 +81,20 @@ fill_table_async({}, "foo", 10):return_to(print_table_size_async) ]] local ExpUtil = require("modules/exp_util") -local Clustorio = require("modules/clusterio/api") +--- @class ExpUtil_Async local Async = { - status = {}, -- Stores the allowed return types from a async function - events = {}, -- Stores all event handlers for this module - _queue_pressure = {}, -- Stores the count of each function in the queue to avoid queue iteration during start_task - _functions = {}, -- Stores a reference to all registered functions + _queue_pressure = {}, --- @type table Stores the count of each function in the queue to avoid queue iteration during start_task + _registered = {}, --- @type table Stores a reference to all registered functions } +--- @class ExpUtil_Async.status: table +--- Stores the allowed return types from a async function +Async.status = {} + --- @class Async.AsyncFunction --- @field id number The id of this async function ---- @operator call: function +--- @operator call: Async.AsyncReturn Async._function_prototype = {} Async._function_metatable = { @@ -170,15 +172,17 @@ end --- Async Function. -- Functions which can be put in storage and used as tasks to be completed over multiple ticks +--- @alias AsyncFunctionOpen fun(...: any): Async.Status?, any?, any? + --- Register a new async function ---- @param func function The function which becomes the async function +--- @param func AsyncFunctionOpen The function which becomes the async function --- @return Async.AsyncFunction # The newly registered async function function Async.register(func) ExpUtil.assert_not_runtime() ExpUtil.assert_argument_type(func, "function", 1, "func") local id = ExpUtil.get_function_name(func) - Async._functions[id] = func + Async._registered[id] = func Async._queue_pressure[id] = 0 return setmetatable({ id = id }, Async._function_metatable) @@ -188,7 +192,7 @@ end --- @param ... any The arguments to call the function with --- @return Async.AsyncReturn function Async._function_prototype:start_soon(...) - assert(Async._functions[self.id], "Async function is not registered") + assert(Async._registered[self.id], "Async function is not registered") Async._queue_pressure[self.id] = Async._queue_pressure[self.id] + 1 return add_to_next_tick(setmetatable({ func_id = self.id, @@ -203,7 +207,7 @@ end function Async._function_prototype:start_after(ticks, ...) ExpUtil.assert_argument_type(ticks, "number", 1, "ticks") assert(ticks > 0, "Ticks must be a positive number") - assert(Async._functions[self.id], "Async function is not registered") + assert(Async._registered[self.id], "Async function is not registered") Async._queue_pressure[self.id] = Async._queue_pressure[self.id] + 1 return add_to_resolve_queue(setmetatable({ func_id = self.id, @@ -216,7 +220,7 @@ end --- @param ... any The arguments to call the function with --- @return Async.AsyncReturn | nil function Async._function_prototype:start_task(...) - assert(Async._functions[self.id], "Async function is not registered") + assert(Async._registered[self.id], "Async function is not registered") if Async._queue_pressure[self.id] > 0 then return end return self:start_soon(...) end @@ -225,8 +229,8 @@ end --- @param ... any The arguments to call the function with --- @return Async.AsyncReturn function Async._function_prototype:start_now(...) - assert(Async._functions[self.id], "Async function is not registered") - local status, rtn1, rtn2 = Async._functions[self.id](...) + assert(Async._registered[self.id], "Async function is not registered") + local status, rtn1, rtn2 = Async._registered[self.id](...) if status == Async.status.continue then return self:start_soon(table.unpack(rtn1)) elseif status == Async.status.delay then @@ -297,7 +301,7 @@ local new_next, new_queue = {}, {} -- File scope to allow for reuse --- @param pending Async.AsyncReturn --- @param tick number local function exec(pending, tick) - local async_func = Async._functions[pending.func_id] + local async_func = Async._registered[pending.func_id] if pending.canceled or async_func == nil then return end local status, rtn1, rtn2 = async_func(table.unpack(pending.args)) if status == Async.status.continue then @@ -363,6 +367,7 @@ local function on_tick() end --- On load, check the queue status and update the pressure values +--- @package function Async.on_load() if storage.exp_async_next == nil then return end resolve_next = storage.exp_async_next @@ -389,6 +394,7 @@ function Async.on_load() end --- On init and server startup initialise the storage data +--- @package function Async.on_init() if storage.exp_async_next == nil then --- @type Async.AsyncReturn[] @@ -399,7 +405,16 @@ function Async.on_init() Async.on_load() end -Async.events[defines.events.on_tick] = on_tick -Async.events[Clustorio.events.on_server_startup] = Async.on_init +local e = defines.events +local events = { + [e.on_tick] = on_tick, +} + +local Clustorio = ExpUtil.optional_require("modules/clusterio/api") +if Clustorio then + events[Clustorio.events.on_server_startup] = Async.on_init +end + Async._function_metatable.__call = Async._function_prototype.start_soon +Async.events = events --- @package return Async diff --git a/exp_util/module/common.lua b/exp_util/module/common.lua deleted file mode 100644 index c0c9c06e..00000000 --- a/exp_util/module/common.lua +++ /dev/null @@ -1,585 +0,0 @@ ---[[-- Util Module - Common -Adds some commonly used functions used in many modules -]] - -local type = type -local assert = assert -local getmetatable = getmetatable -local getinfo = debug.getinfo -local traceback = debug.traceback -local floor = math.floor -local round = math.round -local concat = table.concat -local inspect = table.inspect -local format_string = string.format -local table_to_json = helpers.table_to_json -local write_file = helpers.write_file - -local Common = { - --- A large mapping of colour rgb values by their common name - color = require("modules/exp_util/include/color"), -} - ---- Raise an error if we are not in runtime -function Common.assert_not_runtime() - assert(package.lifecycle ~= package.lifecycle_stage.runtime, "Can not be called during runtime") -end - ---[[local assert_not_closure_fmt = "Can not be called with the closure %s at runtime" ---- Raise an error if a function is a closure and we are in runtime --- @tparam function func The function to assert is not a closure if we are in runtime -function Common.assert_not_closure(func) - assert(package.lifecycle ~= package.lifecycle_stage.runtime, "Can not be called during runtime") - local info = getinfo(2, "nu") - for i = 1, info.nups do - if getupvalue(func, i) ~= "_ENV" then - error(assert_not_closure_fmt:format(info.name or "")) - end - end -end]] - ---- Check the type of a value, also considers LuaObject.object_name and metatable.__class ---- Returns true when the check failed and an error should be raised ---- @param value any The value to check the type of ---- @param type_name string The type name the value should be ---- @return boolean failed True if the check failed and an error should be raised ---- @return string actual_type The actual type of the value -local function check_type(value, type_name) - local value_type = type(value) --[[@as string]] - if value_type == "userdata" then - if type_name == "userdata" then - return false, value_type - end - value_type = value.object_name - elseif value_type == "table" then - if type_name == "table" then - return false, value_type - end - local mt = getmetatable(value) - if mt and mt.__class then - value_type = mt.__class - end - end - return value == nil or value_type ~= type_name, value_type -end - ---- Get the name of a class or object, better than just using type ---- @param value any The value to get the class of ---- @return string # One of type, object_name, __class -function Common.get_class_name(value) - local value_type = type(value) --[[@as string]] - if value_type == "userdata" then - return value.object_name - elseif value_type == "table" then - local mt = getmetatable(value) - if mt and mt.__class then - return mt.__class - end - end - return value_type -end - -local assert_type_fmt = "%s expected to be of type %s but got %s" ---- Raise an error if the type of a value is not as expected ---- @param value any The value to assert the type of ---- @param type_name string The name of the type that value is expected to be ---- @param value_name string? The name of the value being tested, this is included in the error message -function Common.assert_type(value, type_name, value_name) - local failed, actual_type = check_type(value, type_name) - if failed then - error(assert_type_fmt:format(value_name or "Value", type_name, actual_type), 2) - end -end - -local assert_argument_fmt = "Bad argument #%d to %s; %s expected to be of type %s but got %s" ---- Raise an error if the type of any argument is not as expected, more performant than assert_argument_types, but requires more manual input ---- @param arg_value any The argument to assert the type of ---- @param type_name string The name of the type that value is expected to be ---- @param arg_index number The index of the argument being tested, this is included in the error message ---- @param arg_name string? The name of the argument being tested, this is included in the error message -function Common.assert_argument_type(arg_value, type_name, arg_index, arg_name) - local failed, actual_type = check_type(arg_value, type_name) - if failed then - local func_name = getinfo(2, "n").name or "" - error(assert_argument_fmt:format(arg_index, func_name, arg_name or "Argument", type_name, actual_type), 2) - end -end - ---- Write a luu table to a file as a json string, note the defaults are different to write_file ---- @param path string The path to write the json to ---- @param tbl table The table to write to file ---- @param overwrite boolean? When true the json replaces the full contents of the file ---- @param player_index number? The player's machine to write on, -1 means all, 0 is default means host only ---- @return nil -function Common.write_json(path, tbl, overwrite, player_index) - if player_index == -1 then - return write_file(path, table_to_json(tbl) .. "\n", not overwrite) - end - return write_file(path, table_to_json(tbl) .. "\n", not overwrite, player_index or 0) -end - ---- Clear a file by replacing its contents with an empty string ---- @param path string The path to clear the contents of ---- @param player_index number? The player's machine to write on, -1 means all, 0 is default and means host only ---- @return nil -function Common.clear_file(path, player_index) - if player_index == -1 then - return write_file(path, "", false) - end - return write_file(path, "", false, player_index or 0) -end - ---- Same as require but will return nil if the module does not exist, all other errors will propagate to the caller ---- @param module_path string The path to the module to require, same syntax as normal require ---- @return any # The contents of the module, or nil if the module does not exist or did not return a value ---- @deprecated -function Common.optional_require(module_path) - local success, rtn = xpcall(require, traceback, module_path) - if success then return rtn end - if not rtn:find("no such file", 0, true) then - error(rtn, 2) - end -end - ---- Returns a desync sale filepath for a given stack frame, default is the current file ---- @param level number? The level of the stack to get the file of, a value of 1 is the caller of this function ---- @return string # The relative filepath of the given stack frame -function Common.safe_file_path(level) - local debug_info = getinfo((level or 1) + 1, "Sn") - local safe_source = debug_info.source:find("@__level__") - return safe_source == 1 and debug_info.short_src:sub(10, -5) or debug_info.source -end - ---- Returns the name of your module, this assumes your module is stored within /modules (which it is for clustorio) ---- @param level number? The level of the stack to get the module of, a value of 1 is the caller of this function ---- @return string # The name of the module at the given stack frame -function Common.get_module_name(level) - local file_within_module = getinfo((level or 1) + 1, "S").short_src:sub(18, -5) - local next_slash = file_within_module:find("/") - if next_slash then - return file_within_module:sub(1, next_slash - 1) - else - return file_within_module - end -end - ---- Returns the name of a function in a safe and consistent format ---- @param func number | function The level of the stack to get the name of, a value of 1 is the caller of this function ---- @param raw boolean? When true there will not be any < > around the name ---- @return string # The name of the function at the given stack frame or provided as an argument -function Common.get_function_name(func, raw) - local debug_info = getinfo(func, "Sn") - local safe_source = debug_info.source:find("@__level__") - local file_name = safe_source == 1 and debug_info.short_src:sub(10, -5) or debug_info.source - local func_name = debug_info.name or debug_info.linedefined - if raw then return file_name .. ":" .. func_name end - return "<" .. file_name .. ":" .. func_name .. ">" -end - ---- Attempt a simple autocomplete search from a set of options ---- @param options table The table representing the possible options which can be selected ---- @param input string The user input string which should be matched to an option ---- @param use_key boolean? When true the keys will be searched, when false the values will be searched ---- @param rtn_key boolean? When true the selected key will be returned, when false the selected value will be returned ---- @return any # The selected key or value which first matches the input text -function Common.auto_complete(options, input, use_key, rtn_key) - input = input:lower() - if use_key then - for k, v in pairs(options) do - if k:lower():find(input) then - if rtn_key then return k else return v end - end - end - else - for k, v in pairs(options) do - if v:lower():find(input) then - if rtn_key then return k else return v end - end - end - end -end - ---- Formats any value into a safe representation, useful with inspect ---- @param value any The value to be formatted ---- @return LocalisedString # The formatted version of the value ---- @return boolean # True if value is a locale string, nil otherwise -function Common.safe_value(value) - if type(value) == "table" then - local v1 = value[1] - local str = tostring(value) - if type(v1) == "string" and not v1:find("%s") - and (v1 == "" or v1 == "?" or v1:find(".+[.].+")) then - return value, true -- locale string - elseif str ~= "table" then - return str, false -- has __tostring metamethod - else -- plain table - return value, false - end - elseif type(value) == "function" then -- function - return "", false - else -- not: table or function - return tostring(value), false - end -end - ---- @class Common.format_any_param ---- @field as_json boolean? If table values should be returned as json ---- @field max_line_count number? If table newline count exceeds provided then it will be inlined, if 0 then always inline ---- @field no_locale_strings boolean? If value is a locale string it will be treated like a normal table ---- @field depth number? The max depth to process tables to, the default is 5 - ---- Formats any value to be presented in a safe and human readable format ---- @param value any The value to be formatted ---- @param options Common.format_any_param? Options for the formatter ---- @return LocalisedString # The formatted version of the value -function Common.format_any(value, options) - options = options or {} - local formatted, is_locale_string = Common.safe_value(value) - if type(formatted) == "table" and (not is_locale_string or options.no_locale_strings) then - if options.as_json then - local success, rtn = pcall(table_to_json, value) - if success then return rtn end - end - if options.max_line_count ~= 0 then - local rtn = inspect(value, { depth = options.depth or 5, indent = " ", newline = "\n", process = Common.safe_value }) - if options.max_line_count == nil or select(2, rtn:gsub("\n", "")) < options.max_line_count then return rtn end - end - return inspect(value, { depth = options.depth or 5, indent = "", newline = "", process = Common.safe_value }) - end - return formatted -end - ---- @alias Common.format_time_param_format "short" | "long" | "clock" - ---- @class Common.format_time_param_units ---- @field days boolean? True if days are included ---- @field hours boolean? True if hours are included ---- @field minutes boolean? True if minutes are included ---- @field seconds boolean? True if seconds are included - ---- Format a tick value into one of a selection of pre-defined formats (short, long, clock) ---- @param ticks number|nil The number of ticks which will be represented, can be any duration or time value ---- @param format Common.format_time_param_format format to display, must be one of: short, long, clock ---- @param units Common.format_time_param_units A table selecting which units should be displayed, options are: days, hours, minutes, seconds ---- @return string # The ticks formatted into a string of the desired format -function Common.format_time(ticks, format, units) - --- @type string | number, string | number, string | number, string | number - local rtn_days, rtn_hours, rtn_minutes, rtn_seconds = "--", "--", "--", "--" - - if ticks ~= nil then - -- Calculate the values to be determine the display values - local max_days, max_hours, max_minutes, max_seconds = ticks / 5184000, ticks / 216000, ticks / 3600, ticks / 60 - local days, hours = max_days, max_hours - floor(max_days) * 24 - local minutes, seconds = max_minutes - floor(max_hours) * 60, max_seconds - floor(max_minutes) * 60 - - -- Calculate rhw units to be displayed - rtn_days, rtn_hours, rtn_minutes, rtn_seconds = floor(days), floor(hours), floor(minutes), floor(seconds) - if not units.days then rtn_hours = rtn_hours + rtn_days * 24 end - if not units.hours then rtn_minutes = rtn_minutes + rtn_hours * 60 end - if not units.minutes then rtn_seconds = rtn_seconds + rtn_minutes * 60 end - --- @diagnostic enable: cast-local-type - end - - local rtn = {} - if format == "clock" then - -- Example 12:34:56 or --:--:-- - if units.days then rtn[#rtn + 1] = rtn_days end - if units.hours then rtn[#rtn + 1] = rtn_hours end - if units.minutes then rtn[#rtn + 1] = rtn_minutes end - if units.seconds then rtn[#rtn + 1] = rtn_seconds end - return concat(rtn, ":") - elseif format == "short" then - -- Example 12d 34h 56m or --d --h --m - if units.days then rtn[#rtn + 1] = rtn_days .. "d" end - if units.hours then rtn[#rtn + 1] = rtn_hours .. "h" end - if units.minutes then rtn[#rtn + 1] = rtn_minutes .. "m" end - if units.seconds then rtn[#rtn + 1] = rtn_seconds .. "s" end - return concat(rtn, " ") - else - -- Example 12 days, 34 hours, and 56 minutes or -- days, -- hours, and -- minutes - if units.days then rtn[#rtn + 1] = rtn_days .. " days" end - if units.hours then rtn[#rtn + 1] = rtn_hours .. " hours" end - if units.minutes then rtn[#rtn + 1] = rtn_minutes .. " minutes" end - if units.seconds then rtn[#rtn + 1] = rtn_seconds .. " seconds" end - rtn[#rtn] = "and " .. rtn[#rtn] - return concat(rtn, ", ") - end -end - ---- Format a tick value into one of a selection of pre-defined formats (short, long, clock) ---- @param ticks number|nil The number of ticks which will be represented, can be any duration or time value ---- @param format Common.format_time_param_format format to display, must be one of: short, long, clock ---- @param units Common.format_time_param_units A table selecting which units should be displayed, options are: days, hours, minutes, seconds ---- @return LocalisedString # The ticks formatted into a string of the desired format -function Common.format_time_locale(ticks, format, units) - --- @type string | number, string | number, string | number, string | number - local rtn_days, rtn_hours, rtn_minutes, rtn_seconds = "--", "--", "--", "--" - - if ticks ~= nil then - -- Calculate the values to be determine the display values - local max_days, max_hours, max_minutes, max_seconds = ticks / 5184000, ticks / 216000, ticks / 3600, ticks / 60 - local days, hours = max_days, max_hours - floor(max_days) * 24 - local minutes, seconds = max_minutes - floor(max_hours) * 60, max_seconds - floor(max_minutes) * 60 - - -- Calculate rhw units to be displayed - rtn_days, rtn_hours, rtn_minutes, rtn_seconds = floor(days), floor(hours), floor(minutes), floor(seconds) - if not units.days then rtn_hours = rtn_hours + rtn_days * 24 end - if not units.hours then rtn_minutes = rtn_minutes + rtn_hours * 60 end - if not units.minutes then rtn_seconds = rtn_seconds + rtn_minutes * 60 end - end - - local rtn = {} - local join = ", " --- @type LocalisedString - if format == "clock" then - -- Example 12:34:56 or --:--:-- - if units.days then rtn[#rtn + 1] = rtn_days end - if units.hours then rtn[#rtn + 1] = rtn_hours end - if units.minutes then rtn[#rtn + 1] = rtn_minutes end - if units.seconds then rtn[#rtn + 1] = rtn_seconds end - join = { "colon" } - elseif format == "short" then - -- Example 12d 34h 56m or --d --h --m - if units.days then rtn[#rtn + 1] = { "?", { "time-symbol-days-short", rtn_days }, rtn_days .. "d" } end - if units.hours then rtn[#rtn + 1] = { "time-symbol-hours-short", rtn_hours } end - if units.minutes then rtn[#rtn + 1] = { "time-symbol-minutes-short", rtn_minutes } end - if units.seconds then rtn[#rtn + 1] = { "time-symbol-seconds-short", rtn_seconds } end - join = " " - else - -- Example 12 days, 34 hours, and 56 minutes or -- days, -- hours, and -- minutes - if units.days then rtn[#rtn + 1] = { "days", rtn_days } end - if units.hours then rtn[#rtn + 1] = { "hours", rtn_hours } end - if units.minutes then rtn[#rtn + 1] = { "minutes", rtn_minutes } end - if units.seconds then rtn[#rtn + 1] = { "seconds", rtn_seconds } end - rtn[#rtn] = { "", { "and" }, " ", rtn[#rtn] } - end - - --- @type LocalisedString - local joined = { "" } - for k, v in ipairs(rtn) do - joined[2 * k] = v - joined[2 * k + 1] = join - end - - return joined -end - ---- @class Common.format_time_factory_param: Common.format_time_param_units ---- @field format Common.format_time_param_format The format to use ---- @field coefficient number? If present will multiply the input by this amount before formatting - ---- Create a formatter to format a tick value into one of a selection of pre-defined formats (short, long, clock) ---- @param options Common.format_time_factory_param ---- @return fun(ticks: number|nil): string -function Common.format_time_factory(options) - local formatter, format, coefficient = Common.format_time, options.format, options.coefficient - if coefficient then - return function(ticks) return formatter(ticks and ticks * coefficient or nil, format, options) end - end - return function(ticks) return formatter(ticks, format, options) end -end - ---- Create a formatter to format a tick value into one of a selection of pre-defined formats (short, long, clock) ---- @param options Common.format_time_factory_param ---- @return fun(ticks: number|nil): LocalisedString -function Common.format_time_factory_locale(options) - local formatter, format, coefficient = Common.format_time_locale, options.format, options.coefficient - if coefficient then - return function(ticks) return formatter(ticks and ticks * coefficient or nil, format, options) end - end - return function(ticks) return formatter(ticks, format, options) end -end - ---- @class Common.get_or_create_storage_cache ---- @field entities LuaEntity[] Array of found entities matching the search ---- @field current number The current index within the entity array ---- @field count number The number of entities found - ---- @class Common.get_or_create_storage_param: EntitySearchFilters ---- @field item ItemStackIdentification The item stack that must be insertable ---- @field surface LuaSurface The surface to search for targets on ---- @field allow_creation boolean? If new entities can be create to store the items ---- @field cache Common.get_or_create_storage_cache? Internal search cache passed between subsequent calls - ---- Find, or optionally create, a storage entity which a stack can be inserted into ---- @param options Common.get_or_create_storage_param ---- @return LuaEntity -function Common.get_storage_for_stack(options) - local surface = assert(options.surface, "A surface must be provided") - local item = assert(options.item, "An item stack must be provided") - - -- Perform a search if on has not been done already - local cache = options.cache - if cache then - local entities = surface.find_entities_filtered(options) - cache = { - entities = entities, - count = #entities, - current = 0, - } - options.cache = cache - end - --- @cast cache -nil - - -- Find a valid entity from the search results - local current, count, entities = cache.current, cache.count, cache.entities - for i = 1, cache.count do - local entity = entities[((current + i - 1) % count) + 1] - if entity.can_insert(item) then - cache.current = current + 1 - return entity - end - end - - -- No entity was found so one needs to be created - assert(options.allow_creation, "Unable to find valid entity, consider enabling allow_creation") - assert(options.name, "Name must be provided to allow creation of new entities") - - local position - if options.position then - position = surface.find_non_colliding_position(options.name, options.position, options.radius or 0, 1, true) - elseif options.area then - position = surface.find_non_colliding_position_in_box(options.name, options.area, 1, true) - else - position = surface.find_non_colliding_position(options.name, { 0, 0 }, 0, 1, true) - end - assert(position, "Failed to find valid location") - - local entity = surface.create_entity{ name = options.name, position = position, force = options.force or "neutral" } - assert(entity, "Failed to create a new entity") - - cache.count = count + 1 - entities[count] = entity - return entity -end - ---- @class Common.copy_items_to_surface_param: Common.get_or_create_storage_param ---- @field items ItemStackIdentification[] | LuaInventory The item stacks to copy - ---- Insert a copy of the given items into the found entities. If no entities are found then they will be created if possible. ---- @param options Common.copy_items_to_surface_param ---- @return LuaEntity # The last entity inserted into -function Common.copy_items_to_surface(options) - local entity - for item_index = 1, #options.items do - options.item = options.items[item_index] - entity = Common.get_storage_for_stack(options) - entity.insert(options.item) - end - return entity -end - ---- @class Common.move_items_to_surface_param: Common.get_or_create_storage_param ---- @field items LuaItemStack[] The item stacks to move - ---- Insert a copy of the given items into the found entities. If no entities are found then they will be created if possible. ---- @param options Common.move_items_to_surface_param ---- @return LuaEntity # The last entity inserted into -function Common.move_items_to_surface(options) - local entity - for item_index = 1, #options.items do - options.item = options.items[item_index] - entity = Common.get_storage_for_stack(options) - entity.insert(options.item) - options.item.clear() - end - return entity -end - ---- @class Common.transfer_inventory_to_surface_param: Common.copy_items_to_surface_param ---- @field inventory LuaInventory The inventory to transfer - ---- Move the given inventory into the found entities. If no entities are found then they will be created if possible. ---- @param options Common.transfer_inventory_to_surface_param ---- @return LuaEntity # The last entity inserted into -function Common.transfer_inventory_to_surface(options) - options.items = options.inventory - local entity = Common.copy_items_to_surface(options) - options.inventory.clear() - return entity -end - ---- Create an enum table from a set of strings, can use custom indexes to change base ---- @param values { [number]: string } ---- @return { [string | number]: string | number } -function Common.enum(values) - local enum = {} - - local index = 0 -- Real index within values - local offset = 0 -- Offset from base for next index - local base = 0 -- Start point for offset - for k, v in pairs(values) do - index = index + 1 - if k ~= index then - offset = 0 - base = k - end - enum[base + offset] = v - offset = offset + 1 - end - - for k, v in pairs(enum) do - if type(k) == "number" then - enum[v] = k - end - end - - return enum -end - ---- Returns a string for a number with comma separators ---- @param n number ---- @return string -function Common.comma_value(n) -- credit http://richard.warburton.it - local left, num, right = string.match(n, "^([^%d]*%d)(%d*)(.-)$") - return left .. (num:reverse():gsub("(%d%d%d)", "%1, "):reverse()) .. right -end - ---- Returns a message formatted for game chat using rich text colour tags ---- @param message string ---- @param color Color ---- @return string -function Common.format_rich_text_color(message, color) - return format_string( - "[color=%s,%s,%s]%s[/color]", - round(color.r or color[1] or 0, 3), - round(color.g or color[2] or 0, 3), - round(color.b or color[3] or 0, 3), - message - ) -end - ---- Returns a message formatted for game chat using rich text colour tags ---- @param message string ---- @param color Color ---- @return LocalisedString -function Common.format_rich_text_color_locale(message, color) - return { - "color-tag", - round(color.r or color[1] or 0, 3), - round(color.g or color[2] or 0, 3), - round(color.b or color[3] or 0, 3), - message - } -end - ---- Formats a players name using rich text color ---- @param player PlayerIdentification? ---- @return string -function Common.format_player_name(player) - local valid_player = type(player) == "userdata" and player or game.get_player(player --[[@as string|number]]) --[[@as LuaPlayer?]] - local player_name = valid_player and valid_player.name or "" - local player_chat_colour = valid_player and valid_player.chat_color or Common.color.white - return Common.format_rich_text_color(player_name, player_chat_colour) -end - ---- Formats a players name using rich text color ---- @param player PlayerIdentification? ---- @return LocalisedString -function Common.format_player_name_locale(player) - local valid_player = type(player) == "userdata" and player or game.get_player(player --[[@as string|number]]) --[[@as LuaPlayer?]] - local player_name = valid_player and valid_player.name or "" - local player_chat_colour = valid_player and valid_player.chat_color or Common.color.white - return Common.format_rich_text_color_locale(player_name, player_chat_colour) -end - -return Common diff --git a/exp_util/module/flying_text.lua b/exp_util/module/flying_text.lua index 204e459c..3a1bc43f 100644 --- a/exp_util/module/flying_text.lua +++ b/exp_util/module/flying_text.lua @@ -1,8 +1,10 @@ ---[[-- Util Module - FlyingText +--[[-- ExpUtil - FlyingText Provides a method of creating floating text and tags in the world ]] +--- @class ExpUtil_FlyingText local FlyingText = {} + FlyingText.color = require("modules/exp_util/include/color") --- @class FlyingText.create_param:LuaPlayer.create_local_flying_text_param diff --git a/exp_util/module/include/inspect.lua b/exp_util/module/include/inspect.lua index 2b957c25..454a202c 100644 --- a/exp_util/module/include/inspect.lua +++ b/exp_util/module/include/inspect.lua @@ -1,3 +1,5 @@ +--- @diagnostic disable + local inspect = { _VERSION = "inspect.lua 3.1.0", _URL = "http://github.com/kikito/inspect.lua", diff --git a/exp_util/module/include/package.lua b/exp_util/module/include/package.lua index 275ae6ed..8d09dcea 100644 --- a/exp_util/module/include/package.lua +++ b/exp_util/module/include/package.lua @@ -17,6 +17,7 @@ package.lifecycle_stage = { } --- Stores the current lifecycle stage we are in, compare values against package.lifecycle_stage +--- See also: https://forums.factorio.com/viewtopic.php?f=28&t=115622 package.lifecycle = package.lifecycle_stage.control return setmetatable({ @@ -25,6 +26,7 @@ return setmetatable({ on_configuration_changed = function() package.lifecycle = package.lifecycle_stage.config_change end, events = { -- TODO find a reliable way to set to runtime because currently it will desync if accessed before player joined + -- TODO make clusterio optional dependency [defines.events.on_player_joined_game] = function() package.lifecycle = package.lifecycle_stage.runtime end, [Clustorio.events.on_server_startup] = function() package.lifecycle = package.lifecycle_stage.runtime end, }, diff --git a/exp_util/module/include/require.lua b/exp_util/module/include/require.lua index b4cebc2a..d1de99de 100644 --- a/exp_util/module/include/require.lua +++ b/exp_util/module/include/require.lua @@ -1,4 +1,5 @@ -- luacheck:ignore global require +--- @diagnostic disable local package = require("modules/exp_util/include/package") local loaded = package.loaded diff --git a/exp_util/module/include/table.lua b/exp_util/module/include/table.lua index 4ef72595..dbff8b8d 100644 --- a/exp_util/module/include/table.lua +++ b/exp_util/module/include/table.lua @@ -1,16 +1,19 @@ ----@diagnostic disable: duplicate-set-field +--- @diagnostic disable: duplicate-set-field -- luacheck:ignore global table local random = math.random local floor = math.floor local remove = table.remove +local unpack = table.unpack local tonumber = tonumber local pairs = pairs local table_size = table_size --- Adds all keys of the source table to destination table as a shallow copy --- @tparam table dst The table to insert into --- @tparam table src The table to insert from +--- @generic K, V +--- @param dst table Table to insert into +--- @param src table Table to insert from +--- @return table # Table that was passed as the first argument function table.merge(dst, src) local dst_len = #dst for k, v in pairs(src) do @@ -21,75 +24,70 @@ function table.merge(dst, src) dst[k] = v end end + + return dst end ---[[-- Much faster method for inserting items into an array -@tparam table tbl the table that will have the values added to it -@tparam[opt] number start_index the index at which values will be added, nil means end of the array -@tparam table values the new values that will be added to the table -@treturn table the table that was passed as the first argument -@usage-- Adding 1000 values into the middle of the array -local tbl = {} -local values = {} -for i = 1, 1000 do tbl[i] = i values[i] = i end -table.insert_array(tbl, 500, values) -- around 0.4ms -]] -function table.insert_array(tbl, start_index, values) - if not values then - values = start_index +--- Much faster method for inserting items into an array +--- @generic V +--- @param dst V[] Array that will have the values added to it +--- @param start_index number|table|nil Index at which values will be added, nil means end of the array +--- @param src V[]? Array of values that will be added +--- @return V[] # Array that was passed as the first argument +function table.insert_array(dst, start_index, src) + if not src then + assert(type(start_index) == "table") + src = start_index start_index = nil end if start_index then - local starting_length = #tbl - local adding_length = #values + local starting_length = #dst + local adding_length = #src local move_to = start_index + adding_length + 1 for offset = starting_length - start_index, 0, -1 do - tbl[move_to + offset] = tbl[starting_length + offset] + dst[move_to + offset] = dst[starting_length + offset] end start_index = start_index - 1 else - start_index = #tbl + start_index = #dst end - for offset, item in ipairs(values) do - tbl[start_index + offset] = item + for offset, item in ipairs(src) do + dst[start_index + offset] = item end - return tbl + return dst end ---[[-- Much faster method for inserting keys into a table -@tparam table tbl the table that will have keys added to it -@tparam[opt] number start_index the index at which values will be added, nil means end of the array, numbered indexs only -@tparam table tbl2 the table that may contain both string and numbered keys -@treturn table the table passed as the first argument -@usage-- Merging two tables -local tbl = {} -local tbl2 = {} -for i = 1, 100 do tbl[i] = i tbl['_'..i] = i tbl2[i] = i tbl2['__'..i] = i end -table.insert_table(tbl, 50, tbl2) -]] -function table.insert_table(tbl, start_index, tbl2) - if not tbl2 then - tbl2 = start_index +--- Much faster method for inserting keys into a table +--- @generic K, V +--- @param dst table Table that will have keys added to it +--- @param start_index number|table|nil Index at which values will be added, nil means end of the array, numbered indexes only +--- @param src table ? Table that may contain both string and numbered keys +--- @return table # Table passed as the first argument +function table.insert_table(dst, start_index, src) + if not src then + assert(type(start_index) == "table") + src = start_index start_index = nil end - table.insert_array(tbl, start_index, tbl2) - for key, value in pairs(tbl2) do + table.insert_array(dst, start_index, src) + for key, value in pairs(src) do if not tonumber(key) then - tbl[key] = value + dst[key] = value end end - return tbl + return dst end ---- Searches an array to remove a specific element without an index --- @tparam table tbl The array to remove the element from --- @param element The element to search for +--- Searches a table to remove a specific element without an index +--- @generic V +--- @param tbl table Table to remove the element from +--- @param element V Element to remove function table.remove_element(tbl, element) for k, v in pairs(tbl) do if v == element then @@ -100,22 +98,27 @@ function table.remove_element(tbl, element) end --- Removes an item from an array in O(1) time. Does not guarantee the order of elements. --- @tparam table tbl The array to remove the element from --- @tparam number index Must be >= 0. The case where index > #tbl is handled. +--- @generic V +--- @param tbl V[] Array to remove the element from +--- @param index number Must be >= 0. The case where index > #tbl is handled. +--- @return V? # Element which was removed or nil function table.remove_index(tbl, index) local count = #tbl if index > count then return end + local rtn = tbl[count] tbl[index] = tbl[count] tbl[count] = nil + return rtn end --- Return the key which holds this element element --- @tparam table tbl The table to search --- @param element The element to find --- @return The key of the element or nil +--- @generic K, V +--- @param tbl table Table to search +--- @param element V Element to find +--- @return K? # Key of the element or nil function table.get_key(tbl, element) for k, v in pairs(tbl) do if v == element then @@ -127,9 +130,10 @@ function table.get_key(tbl, element) end --- Checks if the arrayed portion of a table contains an element --- @tparam table tbl The table to search --- @param element The element to find --- @treturn ?number The index of the element or nil +--- @generic V +--- @param tbl V[] Table to search +--- @param element V Element to find +--- @return number? # Index of the element or nil function table.get_index(tbl, element) for i = 1, #tbl do if tbl[i] == element then @@ -141,47 +145,48 @@ function table.get_index(tbl, element) end --- Checks if a table contains an element --- @tparam table tbl The table to search --- @param e The element to find --- @treturn boolean True if the element was found +--- @generic V +--- @param tbl table Table to search +--- @param element V Element to find +--- @return boolean # True if the element was found function table.contains(tbl, element) return table.get_key(tbl, element) and true or false end --- Checks if the arrayed portion of a table contains an element --- @tparam table tbl The table to search --- @param e The element to find --- @treturn boolean True if the element was found +--- @generic V +--- @param tbl V[] Table to search +--- @param element V Element to find +--- @return boolean # True if the element was found function table.array_contains(tbl, element) return table.get_index(tbl, element) and true or false end ---[[-- Extracts certain keys from a table, similar to deconstruction in other languages -@tparam table tbl table the which contains the keys -@tparam string ... the names of the keys you want extracted -@return the keys in the order given -@usage -- Deconstruction of a required module -local format_number, distance = table.deconstruct(require('util'), 'format_number', 'distance') -]] +--- Extracts certain keys from a table, similar to deconstruction in other languages +--- @generic K, V +--- @param tbl table Table the which contains the keys +--- @param ... K Keys to extracted +--- @return V ... Values in the order given function table.deconstruct(tbl, ...) local values = {} - for _, key in pairs{ ... } do - table.insert(values, tbl[key]) + for index, key in pairs{ ... } do + values[index] = tbl[key] end - return table.unpack(values) + return unpack(values) end --- Chooses a random entry from a table, can only be used during runtime --- @tparam table tbl The table to select from --- @tparam[opt=false] boolean key When true the key will be returned rather than the value --- @return The selected element from the table -function table.get_random(tbl, key) +--- @generic K, V +--- @param tbl table Table to select from +--- @param rtn_key boolean? True when the key will be returned rather than the value +--- @return K | V # Selected element from the table +function table.get_random(tbl, rtn_key) local target_index = random(1, table_size(tbl)) local count = 1 for k, v in pairs(tbl) do if target_index == count then - if key then + if rtn_key then return k else return v @@ -189,16 +194,18 @@ function table.get_random(tbl, key) end count = count + 1 end + error("Unreachable") end --- Chooses a random entry from a weighted table, can only be used during runtime --- @tparam table weighted_table The table of items and their weights --- @param[opt=1] item_key The index / key of items within each element --- @param[opt=2] weight_key The index / key of the weights within each element --- @return The selected element from the table -function table.get_random_weighted(weighted_table, item_key, weight_index) +--- @generic V, VK, WK +--- @param weighted_table table Table of items and their weights +--- @param value_key VK? Index / key of value to within each element +--- @param weight_index WK? Index / key of the weights within each element +--- @return V # Selected element from the table +function table.get_random_weighted(weighted_table, value_key, weight_index) local total_weight = 0 - item_key = item_key or 1 + value_key = value_key or 1 weight_index = weight_index or 2 for _, w in pairs(weighted_table) do @@ -210,22 +217,23 @@ function table.get_random_weighted(weighted_table, item_key, weight_index) for _, w in pairs(weighted_table) do weight_sum = weight_sum + w[weight_index] if weight_sum >= index then - return w[item_key] + return w[value_key] end end + error("Unreachable") end --- Clears all existing entries in a table --- @tparam table tbl The table to clear --- @tparam[opt=false] boolean array When true only the array portion of the table is cleared -function table.clear(t, array) +--- @param tbl table Table to clear +--- @param array boolean? True when only the array portion of the table is cleared +function table.clear(tbl, array) if array then - for i = 1, #t do - t[i] = nil + for i = 1, #tbl do + tbl[i] = nil end else - for i in pairs(t) do - t[i] = nil + for i in pairs(tbl) do + tbl[i] = nil end end end @@ -233,36 +241,31 @@ end --- Creates a fisher-yates shuffle of a sequential number-indexed table -- because this uses math.random, it cannot be used outside of events if no rng is supplied -- from: http://www.sdknews.com/cross-platform/corona/tutorial-how-to-shuffle-table-items --- @tparam table tbl The table to shuffle --- @tparam[opt=math.random] function rng The function to provide random numbers -function table.shuffle(t, rng) +--- @generic K +--- @param tbl table Table to shuffle +--- @param rng fun(iter: number): K Function to provide random numbers +function table.shuffle(tbl, rng) local rand = rng or math.random - local iterations = #t - if iterations == 0 then - error("Not a sequential table") - return - end + local iterations = assert(#tbl > 0, "Not a sequential table") local j - for i = iterations, 2, -1 do j = rand(i) - t[i], t[j] = t[j], t[i] + tbl[i], tbl[j] = tbl[j], tbl[i] end end --- Default table comparator sort function. --- @local --- @param x one comparator operand --- @param y the other comparator operand --- @return true if x logically comes before y in a list, false otherwise -local function sort_func(x, y) -- sorts tables with mixed index types. - local tx = type(x) - local ty = type(y) +--- @param lhs any LHS comparator operand +--- @param rhs any RHS comparator operand +--- @return boolean True if lhs logically comes before rhs in a list, false otherwise +local function sort_func(lhs, rhs) -- sorts tables with mixed index types. + local tx = type(lhs) + local ty = type(rhs) if tx == ty then - if type(x) == "string" then - return string.lower(x) < string.lower(y) + if type(lhs) == "string" then + return string.lower(lhs) < string.lower(rhs) else - return x < y + return lhs < rhs end elseif tx == "number" then return true -- only x is a number and goes first @@ -272,108 +275,103 @@ local function sort_func(x, y) -- sorts tables with mixed index types. end --- Returns a copy of all of the values in the table. --- @tparam table tbl the to copy the keys from, or an empty table if tbl is nil --- @tparam[opt] boolean sorted whether to sort the keys (slower) or keep the random order from pairs() --- @tparam[opt] boolean as_string whether to try and parse the values as strings, or leave them as their existing type --- @treturn array an array with a copy of all the values in the table +--- @generic V +--- @param tbl table Table to copy the values from, or an empty table if tbl is nil +--- @param sorted boolean? True to sort the keys (slower) or keep the random order from pairs() +--- @param as_string boolean? True to try and parse the values as strings, or leave them as their existing type +--- @return V[] # Array with a copy of all the values in the table function table.get_values(tbl, sorted, as_string) if not tbl then return {} end - local valueset = {} + local value_set = {} local n = 0 - if as_string then -- checking as_string /before/ looping is faster + if as_string then -- checking as_string before looping is faster for _, v in pairs(tbl) do n = n + 1 - valueset[n] = tostring(v) + value_set[n] = tostring(v) end else for _, v in pairs(tbl) do n = n + 1 - valueset[n] = v + value_set[n] = v end end if sorted then - table.sort(valueset, sort_func) + table.sort(value_set, sort_func) end - return valueset + return value_set end --- Returns a copy of all of the keys in the table. --- @tparam table tbl the to copy the keys from, or an empty table if tbl is nil --- @tparam[opt] boolean sorted whether to sort the keys (slower) or keep the random order from pairs() --- @tparam[opt] boolean as_string whether to try and parse the keys as strings, or leave them as their existing type --- @treturn array an array with a copy of all the keys in the table +--- @generic K +--- @param tbl table Table to copy the keys from, or an empty table if tbl is nil +--- @param sorted boolean? True to sort the keys (slower) or keep the random order from pairs() +--- @param as_string boolean? True to try and parse the keys as strings, or leave them as their existing type +--- @return K[] # Array with a copy of all the keys in the table function table.get_keys(tbl, sorted, as_string) if not tbl then return {} end - local keyset = {} + local key_set = {} local n = 0 if as_string then -- checking as_string /before/ looping is faster for k, _ in pairs(tbl) do n = n + 1 - keyset[n] = tostring(k) + key_set[n] = tostring(k) end else for k, _ in pairs(tbl) do n = n + 1 - keyset[n] = k + key_set[n] = k end end if sorted then - table.sort(keyset, sort_func) + table.sort(key_set, sort_func) end - return keyset + return key_set end --- Returns the list is a sorted way that would be expected by people (this is by key) --- @tparam table tbl the table to be sorted --- @treturn table the sorted table +--- @generic K, V +--- @param tbl table Table to be sorted +--- @return table # Sorted table function table.alphanum_sort(tbl) local o = table.get_keys(tbl) local function padnum(d) local dec, n = string.match(d, "(%.?)0*(.+)") return #dec > 0 and ("%.12f"):format(d) or ("%s%03d%s"):format(dec, #n, n) end + table.sort(o, function(a, b) return tostring(a):gsub("%.?%d+", padnum) .. ("%3d"):format(#b) < tostring(b):gsub("%.?%d+", padnum) .. ("%3d"):format(#a) end) + local _tbl = {} for _, k in pairs(o) do _tbl[k] = tbl[k] end - return _tbl end --- Returns the list is a sorted way that would be expected by people (this is by key) (faster alternative than above) --- @tparam table tbl the table to be sorted --- @treturn table the sorted table +--- @generic K, V +--- @param tbl table Table to be sorted +--- @return table # Sorted table function table.key_sort(tbl) local o = table.get_keys(tbl, true) local _tbl = {} for _, k in pairs(o) do _tbl[k] = tbl[k] end - return _tbl end ---[[ - Returns the index where t[index] == target. - If there is no such index, returns a negative value such that bit32.bnot(value) is - the index that the value should be inserted to keep the list ordered. - t must be a list in ascending order for the return value to be valid. - - Usage example: - local t = {1, 3,5, 7,9} - local x = 5 - local index = table.binary_search(t, x) - if index < 0 then - game.print("value not found, smallest index where t[index] > x is: " .. bit32.bnot(index)) - else - game.print("value found at index: " .. index) - end -]] -function table.binary_search(t, target) +--- Returns the index where t[index] == target. +-- If there is no such index, returns a negative value such that bit32.bnot(value) is +-- the index that the value should be inserted to keep the list ordered. +-- It must be a list in ascending order for the return value to be valid. +--- @generic V +--- @param tbl V[] Array to search +--- @param target V Target to find +--- @return number # Index the value was at +function table.binary_search(tbl, target) -- For some reason bit32.bnot doesn't return negative numbers so I'm using ~x = -1 - x instead. - local lower = 1 - local upper = #t + local upper = #tbl if upper == 0 then return -2 -- ~1 @@ -381,7 +379,7 @@ function table.binary_search(t, target) repeat local mid = floor((lower + upper) * 0.5) - local value = t[mid] + local value = tbl[mid] if value == target then return mid elseif value < target then @@ -394,38 +392,25 @@ function table.binary_search(t, target) return -1 - lower -- ~lower end --- add table-related functions that exist in base factorio/util to the 'table' table +-- Add table-related functions that exist in base factorio/util to the 'table' table require "util" --- Similar to serpent.block, returns a string with a pretty representation of a table. -- Notice: This method is not appropriate for saving/restoring tables. It is meant to be used by the programmer mainly while debugging a program. --- @tparam table root tTe table to serialize --- @tparam table options Options are depth, newline, indent, process --- depth sets the maximum depth that will be printed out. When the max depth is reached, inspect will stop parsing tables and just return {...} --- process is a function which allow altering the passed object before transforming it into a string. --- A typical way to use it would be to remove certain values so that they don't appear at all. --- return the prettied table -table.inspect = require "modules.exp_util.include.inspect" --- @dep modules.exp_util.includes.inspect +table.inspect = require("modules/exp_util/include/inspect").inspect --- Takes a table and returns the number of entries in the table. (Slower than #table, faster than iterating via pairs) table.size = table_size --- Creates a deepcopy of a table. Metatables and LuaObjects inside the table are shallow copies. -- Shallow copies meaning it copies the reference to the object instead of the object itself. --- @tparam table object The object to copy --- @treturn table The copied object table.deep_copy = table.deepcopy -- added by util --- Merges multiple tables. Tables later in the list will overwrite entries from tables earlier in the list. -- Ex. merge({{1, 2, 3}, {[2] = 0}, {[3] = 0}}) will return {1, 0, 0} --- @tparam table tables A table of tables to merge --- @treturn table The merged table table.deep_merge = util.merge --- Determines if two tables are structurally equal. --- @tparam table tbl1 The first table --- @tparam table tbl2 The second table --- @treturn boolean True if the tables are equal table.equals = table.compare -- added by util return table diff --git a/exp_util/module/locale/en.cfg b/exp_util/module/locale/en.cfg index 836825f4..e7f6fc0b 100644 --- a/exp_util/module/locale/en.cfg +++ b/exp_util/module/locale/en.cfg @@ -1 +1,2 @@ -time-symbol-days-short=__1__d \ No newline at end of file +time-symbol-days-short=__1__d +rich-text-color-tag=[color=__1__]__2__[/color] \ No newline at end of file diff --git a/exp_util/module/module_exports.lua b/exp_util/module/module_exports.lua index 172de32a..8bed524c 100644 --- a/exp_util/module/module_exports.lua +++ b/exp_util/module/module_exports.lua @@ -1 +1,611 @@ -return require("modules/exp_util/common") +--[[-- ExpUtil +Adds some commonly used functions used in many modules +]] + +local type = type +local assert = assert +local getmetatable = getmetatable +local getinfo = debug.getinfo +local traceback = debug.traceback +local floor = math.floor +local round = math.round +local concat = table.concat +local inspect = table.inspect +local format_string = string.format +local table_to_json = helpers.table_to_json +local write_file = helpers.write_file + +--- @class ExpUtil +local ExpUtil = { + --- A large mapping of colour rgb values by their common name + color = require("modules/exp_util/include/color"), +} + +--- Raise an error if we are not in runtime +function ExpUtil.assert_not_runtime() + assert(package.lifecycle ~= package.lifecycle_stage.runtime, "Can not be called during runtime") +end + +--- Check the type of a value, also considers LuaObject.object_name and metatable.__class +--- Returns true when the check failed and an error should be raised +--- @param value any The value to check the type of +--- @param type_name string The type name the value should be +--- @return boolean failed True if the check failed and an error should be raised +--- @return string actual_type The actual type of the value +local function check_type(value, type_name) + local value_type = type(value) --[[@as string]] + if value_type == "userdata" then + if type_name == "userdata" then + return false, value_type + end + value_type = value.object_name + elseif value_type == "table" then + if type_name == "table" then + return false, value_type + end + local mt = getmetatable(value) + if mt and mt.__class then + value_type = mt.__class + end + end + return value == nil or value_type ~= type_name, value_type +end + +--- Get the name of a class or object, better than just using type +--- @param value any The value to get the class of +--- @return string # One of type, object_name, __class +function ExpUtil.get_class_name(value) + local value_type = type(value) --[[@as string]] + if value_type == "userdata" then + return value.object_name + elseif value_type == "table" then + local mt = getmetatable(value) + if mt and mt.__class then + return mt.__class + end + end + return value_type +end + +local assert_type_fmt = "%s expected to be of type %s but got %s" +--- Raise an error if the type of a value is not as expected +--- @param value any The value to assert the type of +--- @param type_name string The name of the type that value is expected to be +--- @param value_name string? The name of the value being tested, this is included in the error message +function ExpUtil.assert_type(value, type_name, value_name) + local failed, actual_type = check_type(value, type_name) + if failed then + error(assert_type_fmt:format(value_name or "Value", type_name, actual_type), 2) + end +end + +local assert_argument_fmt = "Bad argument #%d to %s; %s expected to be of type %s but got %s" +--- Raise an error if the type of any argument is not as expected, more performant than assert_argument_types, but requires more manual input +--- @param arg_value any The argument to assert the type of +--- @param type_name string The name of the type that value is expected to be +--- @param arg_index number The index of the argument being tested, this is included in the error message +--- @param arg_name string? The name of the argument being tested, this is included in the error message +function ExpUtil.assert_argument_type(arg_value, type_name, arg_index, arg_name) + local failed, actual_type = check_type(arg_value, type_name) + if failed then + local func_name = getinfo(2, "n").name or "" + error(assert_argument_fmt:format(arg_index, func_name, arg_name or "Argument", type_name, actual_type), 2) + end +end + +--- Write a luu table to a file as a json string, note the defaults are different to write_file +--- @param path string The path to write the json to +--- @param tbl table The table to write to file +--- @param overwrite boolean? When true the json replaces the full contents of the file +--- @param player_index number? The player's machine to write on, -1 means all, 0 is default means host only +--- @return nil +function ExpUtil.write_json(path, tbl, overwrite, player_index) + if player_index == -1 then + return write_file(path, table_to_json(tbl) .. "\n", not overwrite) + end + return write_file(path, table_to_json(tbl) .. "\n", not overwrite, player_index or 0) +end + +--- Clear a file by replacing its contents with an empty string +--- @param path string The path to clear the contents of +--- @param player_index number? The player's machine to write on, -1 means all, 0 is default and means host only +--- @return nil +function ExpUtil.clear_file(path, player_index) + if player_index == -1 then + return write_file(path, "", false) + end + return write_file(path, "", false, player_index or 0) +end + +--- Same as require but will return nil if the module does not exist, all other errors will propagate to the caller +--- @param module_path string The path to the module to require, same syntax as normal require +--- @return any # The contents of the module, or nil if the module does not exist or did not return a value +--- @deprecated +function ExpUtil.optional_require(module_path) + local success, rtn = xpcall(require, traceback, module_path) + if success then return rtn end + if not rtn:find("no such file", 0, true) then + error(rtn, 2) + end +end + +--- Returns a desync sale filepath for a given stack frame, default is the current file +--- @param level number? The level of the stack to get the file of, a value of 1 is the caller of this function +--- @return string # The relative filepath of the given stack frame +function ExpUtil.safe_file_path(level) + local debug_info = getinfo((level or 1) + 1, "Sn") + local safe_source = debug_info.source:find("@__level__") + return safe_source == 1 and debug_info.short_src:sub(10, -5) or debug_info.source +end + +--- Returns the name of your module, this assumes your module is stored within /modules (which it is for clustorio) +--- @param level number? The level of the stack to get the module of, a value of 1 is the caller of this function +--- @return string # The name of the module at the given stack frame +function ExpUtil.get_module_name(level) + local file_within_module = getinfo((level or 1) + 1, "S").short_src:sub(18, -5) + local next_slash = file_within_module:find("/") + if next_slash then + return file_within_module:sub(1, next_slash - 1) + else + return file_within_module + end +end + +--- Returns the name of a function in a safe and consistent format +--- @param func number | function The level of the stack to get the name of, a value of 1 is the caller of this function +--- @param raw boolean? When true there will not be any < > around the name +--- @return string # The name of the function at the given stack frame or provided as an argument +function ExpUtil.get_function_name(func, raw) + local debug_info = getinfo(func, "Sn") + local safe_source = debug_info.source:find("@__level__") + local file_name = safe_source == 1 and debug_info.short_src:sub(10, -5) or debug_info.source + local func_name = debug_info.name or debug_info.linedefined + if raw then return file_name .. ":" .. func_name end + return "<" .. file_name .. ":" .. func_name .. ">" +end + +--- Attempt a simple autocomplete search from a set of options +--- @param options table The table representing the possible options which can be selected +--- @param input string The user input string which should be matched to an option +--- @param use_key boolean? When true the keys will be searched, when false the values will be searched +--- @param rtn_key boolean? When true the selected key will be returned, when false the selected value will be returned +--- @return any # The selected key or value which first matches the input text +function ExpUtil.auto_complete(options, input, use_key, rtn_key) + input = input:lower() + if use_key then + for k, v in pairs(options) do + if k:lower():find(input) then + if rtn_key then return k else return v end + end + end + else + for k, v in pairs(options) do + if v:lower():find(input) then + if rtn_key then return k else return v end + end + end + end +end + +--- Formats any value into a safe representation, useful with inspect +--- @param value any The value to be formatted +--- @return LocalisedString # The formatted version of the value +--- @return boolean # True if value is a locale string, nil otherwise +function ExpUtil.safe_value(value) + if type(value) == "table" then + local v1 = value[1] + local str = tostring(value) + if type(v1) == "string" and not v1:find("%s") + and (v1 == "" or v1 == "?" or v1:find(".+[.].+")) then + return value, true -- locale string + elseif str ~= "table" then + return str, false -- has __tostring metamethod + else -- plain table + return value, false + end + elseif type(value) == "function" then -- function + return "", false + else -- not: table or function + return tostring(value), false + end +end + +--- @class Common.format_any_param +--- @field as_json boolean? If table values should be returned as json +--- @field max_line_count number? If table newline count exceeds provided then it will be inlined, if 0 then always inline +--- @field no_locale_strings boolean? If value is a locale string it will be treated like a normal table +--- @field depth number? The max depth to process tables to, the default is 5 + +--- Formats any value to be presented in a safe and human readable format +--- @param value any The value to be formatted +--- @param options Common.format_any_param? Options for the formatter +--- @return LocalisedString # The formatted version of the value +function ExpUtil.format_any(value, options) + options = options or {} + local formatted, is_locale_string = ExpUtil.safe_value(value) + if type(formatted) == "table" and (not is_locale_string or options.no_locale_strings) then + if options.as_json then + local success, rtn = pcall(table_to_json, value) + if success then return rtn end + end + if options.max_line_count ~= 0 then + local rtn = inspect(value, { depth = options.depth or 5, indent = " ", newline = "\n", process = ExpUtil.safe_value }) + if options.max_line_count == nil or select(2, rtn:gsub("\n", "")) < options.max_line_count then return rtn end + end + return inspect(value, { depth = options.depth or 5, indent = "", newline = "", process = ExpUtil.safe_value }) + end + return formatted +end + +--- @alias Common.format_time_param_format "short" | "long" | "clock" + +--- @class Common.format_time_param_units +--- @field days boolean? True if days are included +--- @field hours boolean? True if hours are included +--- @field minutes boolean? True if minutes are included +--- @field seconds boolean? True if seconds are included + +--- Format a tick value into one of a selection of pre-defined formats (short, long, clock) +--- @param ticks number|nil The number of ticks which will be represented, can be any duration or time value +--- @param format Common.format_time_param_format format to display, must be one of: short, long, clock +--- @param units Common.format_time_param_units A table selecting which units should be displayed, options are: days, hours, minutes, seconds +--- @return string # The ticks formatted into a string of the desired format +function ExpUtil.format_time(ticks, format, units) + --- @type string | number, string | number, string | number, string | number + local rtn_days, rtn_hours, rtn_minutes, rtn_seconds = "--", "--", "--", "--" + + if ticks ~= nil then + -- Calculate the values to be determine the display values + local max_days, max_hours, max_minutes, max_seconds = ticks / 5184000, ticks / 216000, ticks / 3600, ticks / 60 + local days, hours = max_days, max_hours - floor(max_days) * 24 + local minutes, seconds = max_minutes - floor(max_hours) * 60, max_seconds - floor(max_minutes) * 60 + + -- Calculate rhw units to be displayed + rtn_days, rtn_hours, rtn_minutes, rtn_seconds = floor(days), floor(hours), floor(minutes), floor(seconds) + if not units.days then rtn_hours = rtn_hours + rtn_days * 24 end + if not units.hours then rtn_minutes = rtn_minutes + rtn_hours * 60 end + if not units.minutes then rtn_seconds = rtn_seconds + rtn_minutes * 60 end + --- @diagnostic enable: cast-local-type + end + + local rtn = {} + if format == "clock" then + -- Example 12:34:56 or --:--:-- + if units.days then rtn[#rtn + 1] = rtn_days end + if units.hours then rtn[#rtn + 1] = rtn_hours end + if units.minutes then rtn[#rtn + 1] = rtn_minutes end + if units.seconds then rtn[#rtn + 1] = rtn_seconds end + return concat(rtn, ":") + elseif format == "short" then + -- Example 12d 34h 56m or --d --h --m + if units.days then rtn[#rtn + 1] = rtn_days .. "d" end + if units.hours then rtn[#rtn + 1] = rtn_hours .. "h" end + if units.minutes then rtn[#rtn + 1] = rtn_minutes .. "m" end + if units.seconds then rtn[#rtn + 1] = rtn_seconds .. "s" end + return concat(rtn, " ") + else + -- Example 12 days, 34 hours, and 56 minutes or -- days, -- hours, and -- minutes + if units.days then rtn[#rtn + 1] = rtn_days .. " days" end + if units.hours then rtn[#rtn + 1] = rtn_hours .. " hours" end + if units.minutes then rtn[#rtn + 1] = rtn_minutes .. " minutes" end + if units.seconds then rtn[#rtn + 1] = rtn_seconds .. " seconds" end + rtn[#rtn] = "and " .. rtn[#rtn] + return concat(rtn, ", ") + end +end + +--- Format a tick value into one of a selection of pre-defined formats (short, long, clock) +--- @param ticks number|nil The number of ticks which will be represented, can be any duration or time value +--- @param format Common.format_time_param_format format to display, must be one of: short, long, clock +--- @param units Common.format_time_param_units A table selecting which units should be displayed, options are: days, hours, minutes, seconds +--- @return LocalisedString # The ticks formatted into a string of the desired format +function ExpUtil.format_time_locale(ticks, format, units) + --- @type string | number, string | number, string | number, string | number + local rtn_days, rtn_hours, rtn_minutes, rtn_seconds = "--", "--", "--", "--" + + if ticks ~= nil then + -- Calculate the values to be determine the display values + local max_days, max_hours, max_minutes, max_seconds = ticks / 5184000, ticks / 216000, ticks / 3600, ticks / 60 + local days, hours = max_days, max_hours - floor(max_days) * 24 + local minutes, seconds = max_minutes - floor(max_hours) * 60, max_seconds - floor(max_minutes) * 60 + + -- Calculate rhw units to be displayed + rtn_days, rtn_hours, rtn_minutes, rtn_seconds = floor(days), floor(hours), floor(minutes), floor(seconds) + if not units.days then rtn_hours = rtn_hours + rtn_days * 24 end + if not units.hours then rtn_minutes = rtn_minutes + rtn_hours * 60 end + if not units.minutes then rtn_seconds = rtn_seconds + rtn_minutes * 60 end + end + + local rtn = {} + local join = ", " --- @type LocalisedString + if format == "clock" then + -- Example 12:34:56 or --:--:-- + if units.days then rtn[#rtn + 1] = rtn_days end + if units.hours then rtn[#rtn + 1] = rtn_hours end + if units.minutes then rtn[#rtn + 1] = rtn_minutes end + if units.seconds then rtn[#rtn + 1] = rtn_seconds end + join = { "colon" } + elseif format == "short" then + -- Example 12d 34h 56m or --d --h --m + if units.days then rtn[#rtn + 1] = { "?", { "time-symbol-days-short", rtn_days }, rtn_days .. "d" } end + if units.hours then rtn[#rtn + 1] = { "time-symbol-hours-short", rtn_hours } end + if units.minutes then rtn[#rtn + 1] = { "time-symbol-minutes-short", rtn_minutes } end + if units.seconds then rtn[#rtn + 1] = { "time-symbol-seconds-short", rtn_seconds } end + join = " " + else + -- Example 12 days, 34 hours, and 56 minutes or -- days, -- hours, and -- minutes + if units.days then rtn[#rtn + 1] = { "days", rtn_days } end + if units.hours then rtn[#rtn + 1] = { "hours", rtn_hours } end + if units.minutes then rtn[#rtn + 1] = { "minutes", rtn_minutes } end + if units.seconds then rtn[#rtn + 1] = { "seconds", rtn_seconds } end + rtn[#rtn] = { "", { "and" }, " ", rtn[#rtn] } + end + + --- @type LocalisedString + local joined = { "" } + for k, v in ipairs(rtn) do + joined[2 * k] = v + joined[2 * k + 1] = join + end + + return joined +end + +--- @class Common.format_time_factory_param: Common.format_time_param_units +--- @field format Common.format_time_param_format The format to use +--- @field coefficient number? If present will multiply the input by this amount before formatting + +--- Create a formatter to format a tick value into one of a selection of pre-defined formats (short, long, clock) +--- @param options Common.format_time_factory_param +--- @return fun(ticks: number|nil): string +function ExpUtil.format_time_factory(options) + local formatter, format, coefficient = ExpUtil.format_time, options.format, options.coefficient + if coefficient then + return function(ticks) return formatter(ticks and ticks * coefficient or nil, format, options) end + end + return function(ticks) return formatter(ticks, format, options) end +end + +--- Create a formatter to format a tick value into one of a selection of pre-defined formats (short, long, clock) +--- @param options Common.format_time_factory_param +--- @return fun(ticks: number|nil): LocalisedString +function ExpUtil.format_time_factory_locale(options) + local formatter, format, coefficient = ExpUtil.format_time_locale, options.format, options.coefficient + if coefficient then + return function(ticks) return formatter(ticks and ticks * coefficient or nil, format, options) end + end + return function(ticks) return formatter(ticks, format, options) end +end + +--- @class Common.get_or_create_storage_cache +--- @field entities LuaEntity[] Array of found entities matching the search +--- @field current number The current index within the entity array +--- @field count number The number of entities found + +--- @class Common.get_or_create_storage_param: EntitySearchFilters +--- @field item ItemStackIdentification The item stack that must be insertable +--- @field surface LuaSurface The surface to search for targets on +--- @field allow_creation boolean? If new entities can be create to store the items +--- @field cache Common.get_or_create_storage_cache? Internal search cache passed between subsequent calls + +--- Find, or optionally create, a storage entity which a stack can be inserted into +--- @param options Common.get_or_create_storage_param +--- @return LuaEntity +function ExpUtil.get_storage_for_stack(options) + local surface = assert(options.surface, "A surface must be provided") + local item = assert(options.item, "An item stack must be provided") + + -- Perform a search if on has not been done already + local cache = options.cache + if cache then + local entities = surface.find_entities_filtered(options) + cache = { + entities = entities, + count = #entities, + current = 0, + } + options.cache = cache + end + --- @cast cache -nil + + -- Find a valid entity from the search results + local current, count, entities = cache.current, cache.count, cache.entities + for i = 1, cache.count do + local entity = entities[((current + i - 1) % count) + 1] + if entity.can_insert(item) then + cache.current = current + 1 + return entity + end + end + + -- No entity was found so one needs to be created + assert(options.allow_creation, "Unable to find valid entity, consider enabling allow_creation") + assert(options.name, "Name must be provided to allow creation of new entities") + + local position + if options.position then + position = surface.find_non_colliding_position(options.name, options.position, options.radius or 0, 1, true) + elseif options.area then + position = surface.find_non_colliding_position_in_box(options.name, options.area, 1, true) + else + position = surface.find_non_colliding_position(options.name, { 0, 0 }, 0, 1, true) + end + assert(position, "Failed to find valid location") + + local entity = surface.create_entity{ name = options.name, position = position, force = options.force or "neutral" } + assert(entity, "Failed to create a new entity") + + cache.count = count + 1 + entities[count] = entity + return entity +end + +--- @class Common.copy_items_to_surface_param: Common.get_or_create_storage_param +--- @field items ItemStackIdentification[] | LuaInventory The item stacks to copy + +--- Insert a copy of the given items into the found entities. If no entities are found then they will be created if possible. +--- @param options Common.copy_items_to_surface_param +--- @return LuaEntity # The last entity inserted into +function ExpUtil.copy_items_to_surface(options) + local entity + for item_index = 1, #options.items do + options.item = options.items[item_index] + entity = ExpUtil.get_storage_for_stack(options) + entity.insert(options.item) + end + return entity +end + +--- @class Common.move_items_to_surface_param: Common.get_or_create_storage_param +--- @field items LuaItemStack[] The item stacks to move + +--- Insert a copy of the given items into the found entities. If no entities are found then they will be created if possible. +--- @param options Common.move_items_to_surface_param +--- @return LuaEntity # The last entity inserted into +function ExpUtil.move_items_to_surface(options) + local entity + for item_index = 1, #options.items do + options.item = options.items[item_index] + entity = ExpUtil.get_storage_for_stack(options) + entity.insert(options.item) + options.item.clear() + end + return entity +end + +--- @class Common.transfer_inventory_to_surface_param: Common.copy_items_to_surface_param +--- @field inventory LuaInventory The inventory to transfer + +--- Move the given inventory into the found entities. If no entities are found then they will be created if possible. +--- @param options Common.transfer_inventory_to_surface_param +--- @return LuaEntity # The last entity inserted into +function ExpUtil.transfer_inventory_to_surface(options) + options.items = options.inventory + local entity = ExpUtil.copy_items_to_surface(options) + options.inventory.clear() + return entity +end + +--- Create an enum table from a set of strings, can use custom indexes to change base +--- @param values string[] +--- @return table +function ExpUtil.enum(values) + local enum = {} + + local index = 0 -- Real index within values + local offset = 0 -- Offset from base for next index + local base = 0 -- Start point for offset + for k, v in pairs(values) do + index = index + 1 + if k ~= index then + offset = 0 + base = k + end + enum[base + offset] = v + offset = offset + 1 + end + + for k, v in pairs(enum) do + if type(k) == "number" then + enum[v] = k + end + end + + return enum +end + +--- Returns a string for a number with comma separators +--- @param n number +--- @return string +function ExpUtil.comma_value(n) -- credit http://richard.warburton.it + local left, num, right = string.match(n, "^([^%d]*%d)(%d*)(.-)$") + return left .. (num:reverse():gsub("(%d%d%d)", "%1, "):reverse()) .. right +end + +--- Returns a message formatted for game chat using rich text colour tags +--- @param message string +--- @param color Color +--- @return string +function ExpUtil.format_rich_text_color(message, color) + return format_string( + "[color=%s,%s,%s]%s[/color]", + round(color.r or color[1] or 0, 3), + round(color.g or color[2] or 0, 3), + round(color.b or color[3] or 0, 3), + message + ) +end + +--- Returns a message formatted for game chat using rich text colour tags +--- @param message LocalisedString +--- @param color Color +--- @return LocalisedString +function ExpUtil.format_rich_text_color_locale(message, color) + return { + "rich-text-color-tag", + round(color.r or color[1] or 0, 3), + round(color.g or color[2] or 0, 3), + round(color.b or color[3] or 0, 3), + message + } +end + +--- Formats a players name using rich text color +--- @param player PlayerIdentification? +--- @return string +function ExpUtil.format_player_name(player) + local valid_player = type(player) == "userdata" and player or game.get_player(player --[[@as string|number]]) --[[@as LuaPlayer?]] + local player_name = valid_player and valid_player.name or "" + local player_chat_colour = valid_player and valid_player.chat_color or ExpUtil.color.white + return ExpUtil.format_rich_text_color(player_name, player_chat_colour) +end + +--- Formats a players name using rich text color +--- @param player PlayerIdentification? +--- @return LocalisedString +function ExpUtil.format_player_name_locale(player) + local valid_player = type(player) == "userdata" and player or game.get_player(player --[[@as string|number]]) --[[@as LuaPlayer?]] + local player_name = valid_player and valid_player.name or "" + local player_chat_colour = valid_player and valid_player.chat_color or ExpUtil.color.white + return ExpUtil.format_rich_text_color_locale(player_name, player_chat_colour) +end + +--- Teleport a player to a position on a surface +--- @param player LuaPlayer Player to teleport +--- @param surface LuaSurface Destination surface +--- @param position MapPosition Destination position +--- @param vehicle_behaviour "allow"|"disallow"|"dismount"? How to handle players who are in a vehicle, default is dismount +--- @return boolean # True if teleported successfully +function ExpUtil.teleport_player(player, surface, position, vehicle_behaviour) + local found_position = surface.find_non_colliding_position("character", position, 32, 1) + + -- Return false if no new position + if not found_position then + return false + end + + -- Check if the player is in a vehicle + local vehicle = player.vehicle + if not vehicle then + return player.teleport(found_position, surface) + end + + -- Handle different vehicle behaviour + if vehicle_behaviour == "disallow" then + return false + elseif vehicle_behaviour == "dismount" then + player.driving = false + return player.teleport(found_position, surface) + end + + -- Teleport the vehicle, or the player if that fails + local vehicle_position = surface.find_non_colliding_position(vehicle.name, position, 32, 1) + if not vehicle_position or not vehicle.teleport(vehicle_position, surface) then + player.driving = false + return player.teleport(found_position, surface) + end + + return true +end + +return ExpUtil diff --git a/exp_util/module/storage.lua b/exp_util/module/storage.lua index 5b4333ba..15d0776d 100644 --- a/exp_util/module/storage.lua +++ b/exp_util/module/storage.lua @@ -1,15 +1,12 @@ ---[[ Util Module - Storage +--[[ ExpUtil - Storage Provides a method of using storage with the guarantee that keys will not conflict --- Drop in boiler plate: -- Below is a drop in boiler plate which ensures your storage access will not conflict with other modules -local storage = - Storage.register({ - my_table = {}, - my_primitive = 1, - }, function(tbl) - storage = tbl - end) +local storage = {} +Storage.register(storage, function(tbl) + storage = tbl +end) --- Registering new storage tables: -- The boiler plate above is not recommend because it is not descriptive in its function @@ -48,39 +45,37 @@ local my_metatable = Storage.register_metatable("MyMetaTable", { ]] -local Clustorio = require("modules/clusterio/api") local ExpUtil = require("modules/exp_util/common") +--- @class ExpUtil_Storage local Storage = { - --- @package - registered = {}, --- @type { [string]: { init: table, callback: fun(tbl: table) } } Map of all registered values and their initial values + _registered = {}, --- @type table Map of all registered values and their initial values } --- Register a new table to be stored in storage, can only be called once per file, can not be called during runtime ---- @param tbl table The initial value for the table you are registering, this should be a local variable ---- @param callback fun(tbl: table) The callback used to replace local references and metatables ---- @return table # The table passed as the first argument +--- @generic T:table +--- @param tbl T The initial value for the table you are registering, this should be a local variable +--- @param callback fun(tbl: T) The callback used to replace local references and metatables +-- This function does not return the table because the callback can't access the local it would be assigned to function Storage.register(tbl, callback) ExpUtil.assert_not_runtime() ExpUtil.assert_argument_type(tbl, "table", 1, "tbl") ExpUtil.assert_argument_type(callback, "function", 2, "callback") local name = ExpUtil.safe_file_path(2) - if Storage.registered[name] then + if Storage._registered[name] then error("Storage.register can only be called once per file", 2) end - Storage.registered[name] = { + Storage._registered[name] = { init = tbl, callback = callback, } - - return tbl end --- Register a metatable which will be automatically restored during on_load --- @param name string The name of the metatable to register, must be unique within your module ---- @param tbl table The metatable to register +--- @param tbl metatable The metatable to register --- @return table # The metatable passed as the second argument function Storage.register_metatable(name, tbl) local module_name = ExpUtil.get_module_name(2) @@ -91,10 +86,10 @@ end --- Restore aliases on load, we do not need to initialise data during this event --- @package function Storage.on_load() - --- @type { [string]: table } + --- @type table local exp_storage = storage.exp_storage if exp_storage == nil then return end - for name, info in pairs(Storage.registered) do + for name, info in pairs(Storage._registered) do if exp_storage[name] ~= nil then info.callback(exp_storage[name]) end @@ -104,14 +99,14 @@ end --- Event Handler, sets initial values if needed and calls all callbacks --- @package function Storage.on_init() - --- @type { [string]: table } + --- @type table local exp_storage = storage.exp_storage if exp_storage == nil then exp_storage = {} storage.exp_storage = exp_storage end - for name, info in pairs(Storage.registered) do + for name, info in pairs(Storage._registered) do if exp_storage[name] == nil then exp_storage[name] = info.init end @@ -119,9 +114,12 @@ function Storage.on_init() end end ---- @package -Storage.events = { - [Clustorio.events.on_server_startup] = Storage.on_init, -} +local events = {} +local Clustorio = ExpUtil.optional_require("modules/clusterio/api") +if Clustorio then + events[Clustorio.events.on_server_startup] = Storage.on_init +end + +Storage.events = events --- @package return Storage