--[[-- Core Module - Common - Adds some commonly used functions used in many modules @core Common @alias Common ]] local Colours = require("modules/exp_util/include/color") local Game = require("modules.exp_legacy.utils.game") --- @dep utils.game local Common = {} --- Type Checking. -- @section typeCheck --[[-- Asserts the argument is of type test_type @tparam any value the value to be tested @tparam[opt=nil] string test_type the type to test for if not given then it tests for nil @treturn boolean is v of type test_type @usage-- Check for a string value local is_string = type_check(value, 'string') @usage-- Check for a nil value local is_nil = type_check(value) ]] function Common.type_check(value, test_type) return test_type and value and type(value) == test_type or not test_type and not value or false end --[[-- Raises an error if the value is of the wrong type @tparam any value the value that you want to test the type of @tparam string test_type the type that the value should be @tparam string error_message the error message that is returned @tparam number level the level to call the error on (level = 1 is the caller) @treturn boolean true if no error was called @usage-- Raise error if value is not a number type_error(value, 'number', 'Value must be a number') ]] function Common.type_error(value, test_type, error_message, level) level = level and level + 1 or 2 return Common.type_check(value, test_type) or error(error_message, level) end --[[-- Asserts the argument is one of type test_types @param value the variable to check @param test_types the type as a table of strings @treturn boolean true if value is one of test_types @usage-- Check for a string or table local is_string_or_table = multi_type_check(value, {'string', 'table'}) ]] function Common.multi_type_check(value, test_types) local vtype = type(value) for _, arg_type in ipairs(test_types) do if vtype == arg_type then return true end end return false end --[[-- Raises an error if the value is of the wrong type @tparam any value the value that you want to test the type of @tparam table test_types the type as a table of strings @tparam string error_message the error message that is returned @tparam number level the level to call the error on (level = 1 is the caller) @treturn boolean true if no error was called @usage-- Raise error if value is not a string or table multi_type_error('foo', {'string', 'table'}, 'Value must be a string or table') ]] function Common.multi_type_error(value, test_types, error_message, level) level = level and level + 1 or 2 return Common.mult_type_check(value, test_types) or error(error_message, level) end --[[-- Raises an error when the value is the incorrect type, uses a consistent error message format @tparam any value the value that you want to test the type of @tparam string test_type the type that the value should be @tparam number param_number the number param it is @tparam[opt] string param_name the name of the param @treturn boolean true if no error was raised @usage-- Output: "Bad argument #2 to ""; argument is of type string expected number" validate_argument_type(value, 'number', 2) @usage-- Output: "Bad argument #2 to ""; "repeat_count" is of type string expected number" validate_argument_type(value, 'number', 2, 'repeat_count') ]] function Common.validate_argument_type(value, test_type, param_number, param_name) if not Common.test_type(value, test_type) then local function_name = debug.getinfo(2, "n").name or "" local error_message if param_name then error_message = string.format("Bad argument #%d to %q; %q is of type %s expected %s", param_number, function_name, param_name, type(value), test_type) else error_message = string.format("Bad argument #%d to %q; argument is of type %s expected %s", param_number, function_name, type(value), test_type) end return error(error_message, 3) end return true end --[[-- Raises an error when the value is the incorrect type, uses a consistent error message format @tparam any value the value that you want to test the type of @tparam string test_types the types that the value should be @tparam number param_number the number param it is @tparam[opt] string param_name the name of the param @treturn boolean true if no error was raised @usage-- Output: "Bad argument #2 to ""; argument is of type number expected string or table" validate_argument_type(value, {'string', 'table'}, 2) @usage-- Output: "Bad argument #2 to ""; "player" is of type number expected string or table" validate_argument_type(value, {'string', 'table'}, 2, 'player') ]] function Common.validate_argument_multi_type(value, test_types, param_number, param_name) if not Common.multi_type_check(value, test_types) then local function_name = debug.getinfo(2, "n").name or "" local error_message if param_name then error_message = string.format("Bad argument #%2d to %q; %q is of type %s expected %s", param_number, function_name, param_name, type(value), table.concat(test_types, " or ")) else error_message = string.format("Bad argument #%2d to %q; argument is of type %s expected %s", param_number, function_name, type(value), table.concat(test_types, " or ")) end return error(error_message, 3) end return true end --- Will raise an error if called during runtime -- @usage error_if_runtime() function Common.error_if_runtime() if package.lifecycle == 8 then local function_name = debug.getinfo(2, "n").name or "" error(function_name .. " can not be called during runtime", 3) end end --- Value Returns. -- @section valueReturns --[[-- Tests if a string contains a given substring. @tparam string s the string to check for the substring @tparam string contains the substring to test for @treturn boolean true if the substring was found in the string @usage-- Test if a string contains a sub string local found = string_contains(str, 'foo') ]] function Common.string_contains(s, contains) return s and string.find(s, contains) ~= nil end --[[-- Used to resolve a value that could also be a function returning that value @tparam any value the value which you want to test is not nil and if it is a function then call the function @treturn any the value given or returned by value if it is a function @usage-- Default value handling -- if default value is not a function then it is returned -- if default value is a function then it is called with the first argument being self local value = Common.resolve_value(self.defaut_value, self) ]] function Common.resolve_value(value, ...) return value and type(value) == "function" and value(...) or value end --- Converts a varible into its boolean value, nil and false return false -- @treturn boolean the boolean form of the varible -- @usage local bool = cast_bool(var) function Common.cast_bool(var) return var and true or false end --- Returns either the second or third argument based on the first argument -- @usage ternary(input_string == 'test', 'Input is test', 'Input is not test') function Common.ternary(c, t, f) return c and t or f end --- Returns a string for a number with comma seperators -- @usage comma_value(input_number) 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 --[[-- Sets a table element to value while also returning value. @tparam table tbl to change the element of @tparam string key the key to set the value of @tparam any value the value to set the key as @treturn any the value that was set @usage-- Set and return value local value = set_and_return(players, player.name, player.online_time) ]] function Common.set_and_return(tbl, key, value) tbl[key] = value return value end --[[-- Writes a table object to a file in json format @tparam string path the path of the file to write include / to use dir @tparam table tbl the table that will be converted to a json string and wrote to file @usage-- Write a lua table as a json to script-outpt/dump write_json('dump', tbl) ]] function Common.write_json(path, tbl) game.write_file(path, game.table_to_json(tbl) .. "\n", true, 0) end --[[-- Calls a require that will not error if the file is not found @usage local file = opt_require('file.not.present') -- will not cause any error @tparam string path the path that you want to require @return the returns from that file or nil, error if not loaded @usage-- Require a file without causing errors, for when a file might not exist local Module = opt_require 'expcore.common' ]] function Common.opt_require(path) local success, rtn = pcall(require, path) if success then return rtn else return nil, rtn end end --[[-- Returns a desync safe file path for the current file @tparam[opt=0] number offset the offset in the stack to get, 0 is current file @treturn string the file path @usage-- Get the current file path local file_path = get_file_path() ]] function Common.get_file_path(offset) offset = offset or 0 return debug.getinfo(offset + 2, "S").short_src:sub(10, -5) end --[[-- Converts a table to an enum @tparam table tbl table the that will be converted @treturn table the new table that acts like an enum @usage-- Make an enum local colors = enum{ 'red', 'green', 'blue' } ]] function Common.enum(tbl) local rtn = {} for k, v in pairs(tbl) do if type(k) ~= "number" then rtn[v] = k end end for k, v in pairs(tbl) do if type(k) == "number" then table.insert(rtn, v) end end for k, v in pairs(rtn) do rtn[v] = k end return rtn end --[[-- Returns the closest match to the input @tparam table options table a of options for the auto complete @tparam string input string the input that will be completed @tparam[opt=false] boolean use_key when true the keys of options will be used as the options @tparam[opt=false] boolean rtn_key when true the the key will be returned rather than the value @return the list item found that matches the input @usage-- Get the element that includes "foo" local value = auto_complete(tbl, "foo") @usage-- Get the element with a key that includes "foo" local value = auto_complete(tbl, "foo", true) @usage-- Get the key with that includes "foo" local key = auto_complete(tbl, "foo", true, true) ]] function Common.auto_complete(options, input, use_key, rtn_key) if type(input) ~= "string" then return end input = input:lower() for key, value in pairs(options) do local check = use_key and key or value if Common.string_contains(string.lower(check), input) then return rtn_key and key or value end end end --- Formatting. -- @section formatting --[[-- Returns a valid string with the name of the actor of a command. @tparam string player_name the name of the player to use rather than server, used only if game.player is nil @treturn string the name of the current actor @usage-- Get the current actor local player_name = get_actor() ]] function Common.get_actor(player_name) return game.player and game.player.name or player_name or "" end --[[-- Returns a message with valid chat tags to change its colour @tparam string message the message that will be in the output @tparam table color a color which contains r, g, b as its keys @treturn string the message with the color tags included @usage-- Use factorio tags to color a chat message local message = format_chat_colour('Hello, World!', { r=355, g=100, b=100 }) ]] function Common.format_chat_colour(message, color) color = color or Colours.white local color_tag = "[color=" .. math.round(color.r, 3) .. ", " .. math.round(color.g, 3) .. ", " .. math.round(color.b, 3) .. "]" return string.format("%s%s[/color]", color_tag, message) end --[[-- Returns a message with valid chat tags to change its colour, using localization @tparam ?string|table message the message that will be in the output @tparam table color a color which contains r, g, b as its keys @treturn table the message with the color tags included @usage-- Use factorio tags and locale strings to color a chat message local message = format_chat_colour_localized('Hello, World!', { r=355, g=100, b=100 }) ]] function Common.format_chat_colour_localized(message, color) color = color or Colours.white color = math.round(color.r, 3) .. ", " .. math.round(color.g, 3) .. ", " .. math.round(color.b, 3) return { "color-tag", color, message } end --[[-- Returns the players name in the players color @tparam LuaPlayer player the player to use the name and color of @tparam[opt=false] boolean raw_string when true a string is returned rather than a localized string @treturn table the players name with tags for the players color @usage-- Format a players name using the players color as a string local message = format_chat_player_name(game.player, true) ]] function Common.format_chat_player_name(player, raw_string) player = Game.get_player_from_any(player) local player_name = player and player.name or "" local player_chat_colour = player and player.chat_color or Colours.white if raw_string then return Common.format_chat_colour(player_name, player_chat_colour) else return Common.format_chat_colour_localized(player_name, player_chat_colour) end end --[[-- Will return a value of any type to the player/server console, allows colour for in-game players @tparam any value a value of any type that will be returned to the player or console @tparam[opt=defines.colour.white] ?defines.color|string colour the colour of the text for the player, ignored when printing to console @tparam[opt=game.player] LuaPlayer player the player that return will go to, if no game.player then returns to server @usage-- Return a value to the current actor, rcon included player_return('Hello, World!') @usage-- Return a value to the current actor, with color player_return('Hello, World!', 'green') @usage-- Return to a player other than the current player_return('Hello, World!', nil, player) ]] function Common.player_return(value, colour, player) colour = Common.type_check(colour, "table") and colour or Colours[colour] ~= Colours.white and Colours[colour] or Colours.white player = player or game.player -- converts the value to a string local returnAsString if Common.type_check(value, "table") or type(value) == "userdata" then if Common.type_check(value.__self, "userdata") or type(value) == "userdata" then -- value is userdata returnAsString = "Cant Display Userdata" elseif Common.type_check(value[1], "string") and string.find(value[1], ".+[.].+") and not string.find(value[1], "%s") then -- value is a locale string returnAsString = value elseif getmetatable(value) ~= nil and not tostring(value):find("table: 0x") then -- value has a tostring meta method returnAsString = tostring(value) else -- value is a table returnAsString = table.inspect(value, { depth = 5, indent = " ", newline = "\n" }) end elseif Common.type_check(value, "function") then -- value is a function returnAsString = "Cant Display Functions" else returnAsString = tostring(value) end -- returns to the player or the server if player then -- allows any valid player identifier to be used player = Game.get_player_from_any(player) if not player then error("Invalid Player given to player_return", 2) end -- plays a nice sound that is different to normal message sound player.play_sound{ path = "utility/scenario_message" } player.print(returnAsString, colour) else rcon.print(returnAsString) end end --[[-- Formats tick into a clean format, denominations from highest to lowest -- time will use : separates -- when a denomination is false it will overflow into the next one @tparam number ticks the number of ticks that represents a time @tparam table options table a of options to use for the format @treturn string a locale string that can be used @usage-- Output: "0h 5m" local time = format_time(18000, { hours=true, minutes=true, string=true }) @usage-- Output: "0 hours and 5 minutes" local time = format_time(18000, { hours=true, minutes=true, string=true, long=true }) @usage-- Output: "00:05:00" local time = format_time(18000, { hours=true, minutes=true, seconds=true, string=true }) @usage-- Output: "--:--:--" local time = format_time(18000, { hours=true, minutes=true, seconds=true, string=true, null=true }) ]] function Common.format_time(ticks, options) -- Sets up the options options = options or { days = false, hours = true, minutes = true, seconds = false, long = false, time = false, string = false, null = false, } -- Basic numbers that are used in calculations local max_days, max_hours, max_minutes, max_seconds = ticks / 5184000, ticks / 216000, ticks / 3600, ticks / 60 local days, hours = max_days, max_hours - math.floor(max_days) * 24 local minutes, seconds = max_minutes - math.floor(max_hours) * 60, max_seconds - math.floor(max_minutes) * 60 -- Handles overflow of disabled denominations local rtn_days, rtn_hours, rtn_minutes, rtn_seconds = math.floor(days), math.floor(hours), math.floor(minutes), math.floor(seconds) if not options.days then rtn_hours = rtn_hours + rtn_days * 24 end if not options.hours then rtn_minutes = rtn_minutes + rtn_hours * 60 end if not options.minutes then rtn_seconds = rtn_seconds + rtn_minutes * 60 end -- Creates the null time format, does not work with long if options.null and not options.long then rtn_days = "--" rtn_hours = "--" rtn_minutes = "--" rtn_seconds = "--" end -- Format options local suffix = "time-symbol-" local suffix_2 = "-short" if options.long then suffix = "" suffix_2 = "" end local div = options.string and " " or "time-format.simple-format-tagged" if options.time then div = options.string and ":" or "time-format.simple-format-div" suffix = false end -- Adds formatting if suffix ~= false then if options.string then -- format it as a string local long = suffix == "" rtn_days = long and rtn_days .. " days" or rtn_days .. "d" rtn_hours = long and rtn_hours .. " hours" or rtn_hours .. "h" rtn_minutes = long and rtn_minutes .. " minutes" or rtn_minutes .. "m" rtn_seconds = long and rtn_seconds .. " seconds" or rtn_seconds .. "s" else rtn_days = { suffix .. "days" .. suffix_2, rtn_days } rtn_hours = { suffix .. "hours" .. suffix_2, rtn_hours } rtn_minutes = { suffix .. "minutes" .. suffix_2, rtn_minutes } rtn_seconds = { suffix .. "seconds" .. suffix_2, rtn_seconds } end elseif not options.null then -- weather string or not it has same format rtn_days = string.format("%02d", rtn_days) rtn_hours = string.format("%02d", rtn_hours) rtn_minutes = string.format("%02d", rtn_minutes) rtn_seconds = string.format("%02d", rtn_seconds) end -- The final return is construed local rtn local append = function(dom, value) if dom and options.string then rtn = rtn and rtn .. div .. value or value elseif dom then rtn = rtn and { div, rtn, value } or value end end append(options.days, rtn_days) append(options.hours, rtn_hours) append(options.minutes, rtn_minutes) append(options.seconds, rtn_seconds) return rtn end --- Factorio. -- @section factorio --[[-- Copies items to the position and stores them in the closest entity of the type given -- Copies the items by prototype name, but keeps them in the original inventory @tparam table items items which are to be added to the chests, an array of LuaItemStack @tparam[opt=navies] LuaSurface surface the surface that the items will be copied to @tparam[opt={0, 0}] table position the position that the items will be copied to {x=100, y=100} @tparam[opt=32] number radius the radius in which the items are allowed to be placed @tparam[opt=iron-chest] string chest_type the chest type that the items should be copied into @treturn LuaEntity the last chest that had items inserted into it @usage-- Copy all the items in a players inventory and place them in chests at {0, 0} copy_items_stack(game.player.get_main_inventory().get_contents()) ]] function Common.copy_items_stack(items, surface, position, radius, chest_type) chest_type = chest_type or "iron-chest" surface = surface or game.surfaces[1] if position and type(position) ~= "table" then return end if type(items) ~= "table" then return end -- Finds all entities of the given type local p = position or { x = 0, y = 0 } local r = radius or 32 local entities = surface.find_entities_filtered{ area = { { p.x - r, p.y - r }, { p.x + r, p.y + r } }, name = chest_type } or {} local count = #entities local current = 1 -- Makes a new empty chest when it is needed local function make_new_chest() local pos = surface.find_non_colliding_position(chest_type, position, 32, 1) local chest = surface.create_entity{ name = chest_type, position = pos, force = "neutral" } table.insert(entities, chest) count = count + 1 return chest end -- Function used to round robin the items into all chests local function next_chest(item) local chest = entities[current] if count == 0 then return make_new_chest() end if chest.get_inventory(defines.inventory.chest).can_insert(item) then -- If the item can be inserted then the chest is returned current = current + 1 if current > count then current = 1 end return chest else -- Other wise it is removed from the list table.remove(entities, current) count = count - 1 end end -- Inserts the items into the chests local last_chest for i = 1, #items do local item = items[i] if item.valid_for_read then local chest = next_chest(item) if not chest or not chest.valid then return error(string.format("Cant move item %s to %s{%s, %s} no valid chest in radius", item.name, surface.name, p.x, p.y)) end chest.insert(item) last_chest = chest end end return last_chest end --[[-- Moves items to the position and stores them in the closest entity of the type given -- Differs from move_items by accepting a table of LuaItemStack and transferring them into the inventory - not copying @tparam table items items which are to be added to the chests, an array of LuaItemStack @tparam[opt=navies] LuaSurface surface the surface that the items will be moved to @tparam[opt={0, 0}] table position the position that the items will be moved to {x=100, y=100} @tparam[opt=32] number radius the radius in which the items are allowed to be placed @tparam[opt=iron-chest] string chest_type the chest type that the items should be moved into @treturn LuaEntity the last chest that had items inserted into it @usage-- Copy all the items in a players inventory and place them in chests at {0, 0} move_items_stack(game.player.get_main_inventory()) ]] function Common.move_items_stack(items, surface, position, radius, chest_type) chest_type = chest_type or "steel-chest" surface = surface or game.surfaces[1] if position and type(position) ~= "table" then return end if type(items) ~= "table" then return end -- Finds all entities of the given type local p = position or { x = 0, y = 0 } local r = radius or 32 local entities = surface.find_entities_filtered{ area = { { p.x - r, p.y - r }, { p.x + r, p.y + r } }, name = { chest_type, "iron-chest" } } or {} local count = #entities local current = 0 local last_entity = nil -- ipairs does not work on LuaInventory for i = 1, #items do local item = items[i] if item.valid_for_read then local inserted = false -- Attempt to insert the items for j = 1, count do local entity = entities[((current + j - 1) % count) + 1] if entity.can_insert(item) then last_entity = entity current = current + 1 entity.insert(item) inserted = true break end end -- If it was not inserted then a new entity is needed if not inserted then --[[ if not options.allow_creation then error('Unable to insert items into a valid entity, consider enabling allow_creation') end if options.name == nil then error('Name must be provided to allow creation of new entities') end if options.position then pos = surface.find_non_colliding_position(chest_type, p, r, 1, true) elseif options.area then pos = surface.find_non_colliding_position_in_box(chest_type, options.area, 1, true) else pos = surface.find_non_colliding_position(chest_type, {0,0}, 0, 1, true) end ]] local pos = surface.find_non_colliding_position(chest_type, p, r, 1, true) last_entity = surface.create_entity{ name = chest_type, position = pos, force = "neutral" } count = count + 1 entities[count] = last_entity last_entity.insert(item) end end end --[[ -- Makes a new empty chest when it is needed local function make_new_chest() local pos = surface.find_non_colliding_position(chest_type, position, 32, 1) local chest = surface.create_entity{name=chest_type, position=pos, force='neutral'} table.insert(entities, chest) count = count + 1 return chest end -- Function used to round robin the items into all chests local function next_chest(item) local chest = entities[current] if count == 0 then return make_new_chest() end if chest.get_inventory(defines.inventory.chest).can_insert(item) then -- If the item can be inserted then the chest is returned current = current + 1 if current > count then current = 1 end return chest else -- Other wise it is removed from the list table.remove(entities, current) count = count - 1 end end -- Inserts the items into the chests local last_chest for i=1,#items do local item = items[i] if item.valid_for_read then local chest = next_chest(item) if not chest or not chest.valid then return error(string.format('Cant move item %s to %s{%s, %s} no valid chest in radius', item.name, surface.name, p.x, p.y)) end local empty_stack = chest.get_inventory(defines.inventory.chest).find_empty_stack(item.name) if not empty_stack then return error(string.format('Cant move item %s to %s{%s, %s} no valid chest in radius', item.name, surface.name, p.x, p.y)) end empty_stack.transfer_stack(item) last_chest = chest end end return last_chest ]] return last_entity end --[[-- Prints a colored value on a location, color is based on the value. nb: src is below but the gradent has been edited https://github.com/Refactorio/RedMew/blob/9184b2940f311d8c9c891e83429fc57ec7e0c4a2/map_gen/maps/diggy/debug.lua#L31 @tparam number value the value to show must be between -1 and 1, scale can be used to achive this @tparam LuaSurface surface the surface to palce the value on @tparam table position {x, y} the possition to palce the value at @tparam[opt=1] number scale how much to scale the colours by @tparam[opt=0] number offset the offset in the +x +y direction @tparam[opt=false] boolean immutable if immutable, only set, never do a surface lookup, values never change @usage-- Place a 0 at {0, 0} print_grid_value(0, game.player.surface, { x=0, y=0 }) ]] function Common.print_grid_value(value, surface, position, scale, offset, immutable) local is_string = type(value) == "string" local color = Colours.white local text = value if type(immutable) ~= "boolean" then immutable = false end if not is_string then scale = scale or 1 offset = offset or 0 position = { x = position.x + offset, y = position.y + offset } local r = math.clamp(-value / scale, 0, 1) local g = math.clamp(1 - math.abs(value) / scale, 0, 1) local b = math.clamp(value / scale, 0, 1) color = { r = r, g = g, b = b } -- round at precision of 2 text = math.floor(100 * value) * 0.01 if (0 == text) then text = "0.00" end end if not immutable then local text_entity = surface.find_entity("flying-text", position) if text_entity then text_entity.text = text text_entity.color = color return end end surface.create_entity{ name = "flying-text", color = color, text = text, position = position, }.active = false end --[[-- Clears all flying text entities on a surface @tparam LuaSurface surface the surface to clear @usage-- Remove all flying text on the surface clear_flying_text(game.player.surface) ]] function Common.clear_flying_text(surface) local entities = surface.find_entities_filtered{ name = "flying-text" } for _, entity in pairs(entities) do if entity and entity.valid then entity.destroy() end end end return Common