mirror of
https://github.com/PHIDIAS0303/ExpCluster.git
synced 2025-12-27 11:35:22 +09:00
Removed useless files
This commit is contained in:
@@ -1,179 +0,0 @@
|
||||
--[[-- info
|
||||
Original (javascript) version: https://hastebin.com/udakacavap.js
|
||||
Can be tested against: https://wiki.factorio.com/Enemies#Spawn_chances_by_evolution_factor
|
||||
]]
|
||||
|
||||
-- dependencies
|
||||
local Global = require 'utils.global' --- @dep utils.global
|
||||
local Debug = require 'utils.debug' --- @dep utils.debug
|
||||
local table = require 'utils.table' --- @dep utils.table
|
||||
|
||||
-- localized functions
|
||||
local get_random_weighted = table.get_random_weighted
|
||||
local round = math.round
|
||||
local ceil = math.ceil
|
||||
local floor = math.floor
|
||||
local random = math.random
|
||||
local pairs = pairs
|
||||
local format = string.format
|
||||
|
||||
-- this
|
||||
local AlienEvolutionProgress = {}
|
||||
|
||||
local memory = {
|
||||
spawner_specifications = {},
|
||||
spawner_specifications_count = 0,
|
||||
evolution_cache = {
|
||||
['biter-spawner'] = {
|
||||
evolution = -1,
|
||||
weight_table = {},
|
||||
},
|
||||
['spitters-spawner'] = {
|
||||
evolution = -1,
|
||||
weight_table = {},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
Global.register_init({
|
||||
memory = memory,
|
||||
}, function(tbl)
|
||||
for name, prototype in pairs(game.entity_prototypes) do
|
||||
if prototype.type == 'unit-spawner' and prototype.subgroup.name == 'enemies' then
|
||||
tbl.memory.spawner_specifications[name] = prototype.result_units
|
||||
memory.spawner_specifications_count = memory.spawner_specifications_count + 1
|
||||
end
|
||||
end
|
||||
end, function(tbl)
|
||||
memory = tbl.memory
|
||||
end)
|
||||
|
||||
local function lerp(low, high, pos)
|
||||
local s = high.evolution_factor - low.evolution_factor;
|
||||
local l = (pos - low.evolution_factor) / s;
|
||||
return (low.weight * (1 - l)) + (high.weight * l)
|
||||
end
|
||||
|
||||
local function get_values(map, evolution_factor)
|
||||
local result = {}
|
||||
local sum = 0
|
||||
|
||||
for _, spawner_data in pairs(map) do
|
||||
local list = spawner_data.spawn_points;
|
||||
local low = list[1];
|
||||
local high = list[#list];
|
||||
|
||||
for _, val in pairs(list) do
|
||||
local val_evolution = val.evolution_factor
|
||||
if val_evolution <= evolution_factor and val_evolution > low.evolution_factor then
|
||||
low = val;
|
||||
end
|
||||
if val_evolution >= evolution_factor and val_evolution < high.evolution_factor then
|
||||
high = val
|
||||
end
|
||||
end
|
||||
|
||||
local val
|
||||
if evolution_factor <= low.evolution_factor then
|
||||
val = low.weight
|
||||
elseif evolution_factor >= high.evolution_factor then
|
||||
val = high.weight;
|
||||
else
|
||||
val = lerp(low, high, evolution_factor)
|
||||
end
|
||||
sum = sum + val;
|
||||
|
||||
result[spawner_data.unit] = val;
|
||||
end
|
||||
|
||||
local weighted_table = {}
|
||||
local count = 0
|
||||
for index, _ in pairs(result) do
|
||||
count = count + 1
|
||||
weighted_table[count] = {index, result[index] / sum}
|
||||
end
|
||||
|
||||
return weighted_table;
|
||||
end
|
||||
|
||||
local function get_spawner_values(spawner, evolution)
|
||||
local spawner_specification = memory.spawner_specifications[spawner]
|
||||
if not spawner_specification then
|
||||
Debug.print(format('Spawner "%s" does not exist in the prototype data', spawner))
|
||||
return
|
||||
end
|
||||
|
||||
local cache = memory.evolution_cache[spawner]
|
||||
|
||||
if not cache then
|
||||
cache = {
|
||||
evolution = -1,
|
||||
weight_table = {},
|
||||
}
|
||||
memory.evolution_cache[spawner] = cache
|
||||
end
|
||||
|
||||
local evolution_value = round(evolution * 100)
|
||||
if (cache.evolution < evolution_value) then
|
||||
cache.evolution = evolution_value
|
||||
cache.weight_table = get_values(spawner_specification, evolution)
|
||||
end
|
||||
|
||||
return cache.weight_table
|
||||
end
|
||||
|
||||
local function calculate_total(count, spawner, evolution)
|
||||
if count == 0 then
|
||||
return {}
|
||||
end
|
||||
|
||||
local spawner_values = get_spawner_values(spawner, evolution)
|
||||
if not spawner_values then
|
||||
return {}
|
||||
end
|
||||
|
||||
local aliens = {}
|
||||
for _ = 1, count do
|
||||
local name = get_random_weighted(spawner_values)
|
||||
aliens[name] = (aliens[name] or 0) + 1
|
||||
end
|
||||
|
||||
return aliens
|
||||
end
|
||||
|
||||
---Creates the spawner_request structure required for AlienEvolutionProgress.get_aliens for all
|
||||
---available spawner's. If dividing the total spawner's by the total aliens causes a fraction, the
|
||||
---fraction will decide a chance to spawn. 1 alien for 2 spawner's will have 50% on both.
|
||||
---@param total_aliens table
|
||||
function AlienEvolutionProgress.create_spawner_request(total_aliens)
|
||||
local per_spawner = total_aliens / memory.spawner_specifications_count
|
||||
local fraction = per_spawner % 1
|
||||
|
||||
local spawner_request = {}
|
||||
for spawner, _ in pairs(memory.spawner_specifications) do
|
||||
local count = per_spawner
|
||||
if fraction > 0 then
|
||||
if random() > fraction then
|
||||
count = ceil(count)
|
||||
else
|
||||
count = floor(count)
|
||||
end
|
||||
end
|
||||
spawner_request[spawner] = count
|
||||
end
|
||||
|
||||
return spawner_request
|
||||
end
|
||||
|
||||
function AlienEvolutionProgress.get_aliens(spawner_requests, evolution)
|
||||
local aliens = {}
|
||||
for spawner, count in pairs(spawner_requests) do
|
||||
for name, amount in pairs(calculate_total(count, spawner, evolution)) do
|
||||
aliens[name] = (aliens[name] or 0) + amount
|
||||
end
|
||||
end
|
||||
|
||||
return aliens
|
||||
end
|
||||
|
||||
return AlienEvolutionProgress
|
||||
163
utils/color_presets.lua
Normal file
163
utils/color_presets.lua
Normal file
@@ -0,0 +1,163 @@
|
||||
-- source: https://www.rapidtables.com/web/color/RGB_Color.html
|
||||
return {
|
||||
maroon = {r = 128, g = 0, b = 0},
|
||||
dark_red = {r = 139, g = 0, b = 0},
|
||||
brown = {r = 165, g = 42, b = 42},
|
||||
firebrick = {r = 178, g = 34, b = 34},
|
||||
crimson = {r = 220, g = 20, b = 60},
|
||||
red = {r = 255, g = 0, b = 0},
|
||||
tomato = {r = 255, g = 99, b = 71},
|
||||
coral = {r = 255, g = 127, b = 80},
|
||||
indian_red = {r = 205, g = 92, b = 92},
|
||||
light_coral = {r = 240, g = 128, b = 128},
|
||||
dark_salmon = {r = 233, g = 150, b = 122},
|
||||
salmon = {r = 250, g = 128, b = 114},
|
||||
light_salmon = {r = 255, g = 160, b = 122},
|
||||
orange_red = {r = 255, g = 69, b = 0},
|
||||
dark_orange = {r = 255, g = 140, b = 0},
|
||||
orange = {r = 255, g = 165, b = 0},
|
||||
gold = {r = 255, g = 215, b = 0},
|
||||
dark_golden_rod = {r = 184, g = 134, b = 11},
|
||||
golden_rod = {r = 218, g = 165, b = 32},
|
||||
pale_golden_rod = {r = 238, g = 232, b = 170},
|
||||
dark_khaki = {r = 189, g = 183, b = 107},
|
||||
khaki = {r = 240, g = 230, b = 140},
|
||||
olive = {r = 128, g = 128, b = 0},
|
||||
yellow = {r = 255, g = 255, b = 0},
|
||||
yellow_green = {r = 154, g = 205, b = 50},
|
||||
dark_olive_green = {r = 85, g = 107, b = 47},
|
||||
olive_drab = {r = 107, g = 142, b = 35},
|
||||
lawn_green = {r = 124, g = 252, b = 0},
|
||||
chart_reuse = {r = 127, g = 255, b = 0},
|
||||
green_yellow = {r = 173, g = 255, b = 47},
|
||||
dark_green = {r = 0, g = 100, b = 0},
|
||||
green = {r = 0, g = 128, b = 0},
|
||||
forest_green = {r = 34, g = 139, b = 34},
|
||||
lime = {r = 0, g = 255, b = 0},
|
||||
lime_green = {r = 50, g = 205, b = 50},
|
||||
light_green = {r = 144, g = 238, b = 144},
|
||||
pale_green = {r = 152, g = 251, b = 152},
|
||||
dark_sea_green = {r = 143, g = 188, b = 143},
|
||||
medium_spring_green = {r = 0, g = 250, b = 154},
|
||||
spring_green = {r = 0, g = 255, b = 127},
|
||||
sea_green = {r = 46, g = 139, b = 87},
|
||||
medium_aqua_marine = {r = 102, g = 205, b = 170},
|
||||
medium_sea_green = {r = 60, g = 179, b = 113},
|
||||
light_sea_green = {r = 32, g = 178, b = 170},
|
||||
dark_slate_gray = {r = 47, g = 79, b = 79},
|
||||
teal = {r = 0, g = 128, b = 128},
|
||||
dark_cyan = {r = 0, g = 139, b = 139},
|
||||
aqua = {r = 0, g = 255, b = 255},
|
||||
cyan = {r = 0, g = 255, b = 255},
|
||||
light_cyan = {r = 224, g = 255, b = 255},
|
||||
dark_turquoise = {r = 0, g = 206, b = 209},
|
||||
turquoise = {r = 64, g = 224, b = 208},
|
||||
medium_turquoise = {r = 72, g = 209, b = 204},
|
||||
pale_turquoise = {r = 175, g = 238, b = 238},
|
||||
aqua_marine = {r = 127, g = 255, b = 212},
|
||||
powder_blue = {r = 176, g = 224, b = 230},
|
||||
cadet_blue = {r = 95, g = 158, b = 160},
|
||||
steel_blue = {r = 70, g = 130, b = 180},
|
||||
corn_flower_blue = {r = 100, g = 149, b = 237},
|
||||
deep_sky_blue = {r = 0, g = 191, b = 255},
|
||||
dodger_blue = {r = 30, g = 144, b = 255},
|
||||
light_blue = {r = 173, g = 216, b = 230},
|
||||
sky_blue = {r = 135, g = 206, b = 235},
|
||||
light_sky_blue = {r = 135, g = 206, b = 250},
|
||||
midnight_blue = {r = 25, g = 25, b = 112},
|
||||
navy = {r = 0, g = 0, b = 128},
|
||||
dark_blue = {r = 0, g = 0, b = 139},
|
||||
medium_blue = {r = 0, g = 0, b = 205},
|
||||
blue = {r = 0, g = 0, b = 255},
|
||||
royal_blue = {r = 65, g = 105, b = 225},
|
||||
blue_violet = {r = 138, g = 43, b = 226},
|
||||
indigo = {r = 75, g = 0, b = 130},
|
||||
dark_slate_blue = {r = 72, g = 61, b = 139},
|
||||
slate_blue = {r = 106, g = 90, b = 205},
|
||||
medium_slate_blue = {r = 123, g = 104, b = 238},
|
||||
medium_purple = {r = 147, g = 112, b = 219},
|
||||
dark_magenta = {r = 139, g = 0, b = 139},
|
||||
dark_violet = {r = 148, g = 0, b = 211},
|
||||
dark_orchid = {r = 153, g = 50, b = 204},
|
||||
medium_orchid = {r = 186, g = 85, b = 211},
|
||||
purple = {r = 128, g = 0, b = 128},
|
||||
thistle = {r = 216, g = 191, b = 216},
|
||||
plum = {r = 221, g = 160, b = 221},
|
||||
violet = {r = 238, g = 130, b = 238},
|
||||
magenta = {r = 255, g = 0, b = 255},
|
||||
fuchsia = {r = 255, g = 0, b = 255},
|
||||
orchid = {r = 218, g = 112, b = 214},
|
||||
medium_violet_red = {r = 199, g = 21, b = 133},
|
||||
pale_violet_red = {r = 219, g = 112, b = 147},
|
||||
deep_pink = {r = 255, g = 20, b = 147},
|
||||
hot_pink = {r = 255, g = 105, b = 180},
|
||||
light_pink = {r = 255, g = 182, b = 193},
|
||||
pink = {r = 255, g = 192, b = 203},
|
||||
antique_white = {r = 250, g = 235, b = 215},
|
||||
beige = {r = 245, g = 245, b = 220},
|
||||
bisque = {r = 255, g = 228, b = 196},
|
||||
blanched_almond = {r = 255, g = 235, b = 205},
|
||||
wheat = {r = 245, g = 222, b = 179},
|
||||
corn_silk = {r = 255, g = 248, b = 220},
|
||||
lemon_chiffon = {r = 255, g = 250, b = 205},
|
||||
light_golden_rod_yellow = {r = 250, g = 250, b = 210},
|
||||
light_yellow = {r = 255, g = 255, b = 224},
|
||||
saddle_brown = {r = 139, g = 69, b = 19},
|
||||
sienna = {r = 160, g = 82, b = 45},
|
||||
chocolate = {r = 210, g = 105, b = 30},
|
||||
peru = {r = 205, g = 133, b = 63},
|
||||
sandy_brown = {r = 244, g = 164, b = 96},
|
||||
burly_wood = {r = 222, g = 184, b = 135},
|
||||
tan = {r = 210, g = 180, b = 140},
|
||||
rosy_brown = {r = 188, g = 143, b = 143},
|
||||
moccasin = {r = 255, g = 228, b = 181},
|
||||
navajo_white = {r = 255, g = 222, b = 173},
|
||||
peach_puff = {r = 255, g = 218, b = 185},
|
||||
misty_rose = {r = 255, g = 228, b = 225},
|
||||
lavender_blush = {r = 255, g = 240, b = 245},
|
||||
linen = {r = 250, g = 240, b = 230},
|
||||
old_lace = {r = 253, g = 245, b = 230},
|
||||
papaya_whip = {r = 255, g = 239, b = 213},
|
||||
sea_shell = {r = 255, g = 245, b = 238},
|
||||
mint_cream = {r = 245, g = 255, b = 250},
|
||||
slate_gray = {r = 112, g = 128, b = 144},
|
||||
light_slate_gray = {r = 119, g = 136, b = 153},
|
||||
light_steel_blue = {r = 176, g = 196, b = 222},
|
||||
lavender = {r = 230, g = 230, b = 250},
|
||||
floral_white = {r = 255, g = 250, b = 240},
|
||||
alice_blue = {r = 240, g = 248, b = 255},
|
||||
ghost_white = {r = 248, g = 248, b = 255},
|
||||
honeydew = {r = 240, g = 255, b = 240},
|
||||
ivory = {r = 255, g = 255, b = 240},
|
||||
azure = {r = 240, g = 255, b = 255},
|
||||
snow = {r = 255, g = 250, b = 250},
|
||||
black = {r = 0, g = 0, b = 0},
|
||||
silver = {r = 192, g = 192, b = 192},
|
||||
dim_grey = {r = 105, g = 105, b = 105},
|
||||
dim_gray = {r = 105, g = 105, b = 105},
|
||||
grey = {r = 128, g = 128, b = 128},
|
||||
gray = {r = 128, g = 128, b = 128},
|
||||
dark_grey = {r = 169, g = 169, b = 169},
|
||||
dark_gray = {r = 169, g = 169, b = 169},
|
||||
light_grey = {r = 211, g = 211, b = 211},
|
||||
light_gray = {r = 211, g = 211, b = 211},
|
||||
gainsboro = {r = 220, g = 220, b = 220},
|
||||
white_smoke = {r = 245, g = 245, b = 245},
|
||||
white = {r = 255, g = 255, b = 255},
|
||||
jailed = {r = 255, g = 255, b = 255},
|
||||
probation = {r = 255, g = 255, b = 255},
|
||||
guest = {r = 255, g = 255, b = 255},
|
||||
auto_trusted = {r = 192, g = 192, b = 192},
|
||||
regular = {r = 0.155, g = 0.540, b = 0.898},
|
||||
admin = {r = 0.093, g = 0.768, b = 0.172},
|
||||
donator = {r = 172.6, g = 70.2, b = 215.8},
|
||||
[-10] = {r = 255, g = 255, b = 255}, -- probation
|
||||
[0] = {r = 255, g = 255, b = 255}, -- guest
|
||||
[10] = {r = 192, g = 192, b = 192}, -- auto_trusted
|
||||
[20] = {r = 0.155, g = 0.540, b = 0.898}, -- regular
|
||||
[30] = {r = 0.093, g = 0.768, b = 0.172}, -- admin
|
||||
success = {r = 0, g = 255, b = 0},
|
||||
warning = {r = 255, g = 255, b = 0},
|
||||
fail = {r = 255, g = 0, b = 0},
|
||||
info = {r = 255, g = 255, b = 255}
|
||||
}
|
||||
@@ -1,326 +0,0 @@
|
||||
local Event = require 'utils.event' --- @dep utils.event
|
||||
local Game = require 'utils.game' --- @dep utils.game
|
||||
local Utils = require 'utils.core' --- @dep utils.core
|
||||
local Timestamp = require 'utils.timestamp' --- @dep utils.timestamp
|
||||
local Rank = require 'features.rank_system' --- @dep features.rank_system
|
||||
local Donator = require 'features.donator' --- @dep features.donator
|
||||
local Server = require 'features.server' --- @dep features.server
|
||||
local Ranks = require 'resources.ranks' --- @dep resources.ranks
|
||||
|
||||
local insert = table.insert
|
||||
local format = string.format
|
||||
local next = next
|
||||
local serialize = serpent.line
|
||||
local match = string.match
|
||||
local gmatch = string.gmatch
|
||||
local get_rank_name = Rank.get_rank_name
|
||||
|
||||
local Command = {}
|
||||
|
||||
local deprecated_command_alternatives = {
|
||||
['silent-command'] = 'sc',
|
||||
['tpplayer'] = 'tp <player>',
|
||||
['tppos'] = 'tp',
|
||||
['tpmode'] = 'tp mode',
|
||||
['color-redmew'] = 'redmew-color'
|
||||
}
|
||||
|
||||
local notify_on_commands = {
|
||||
['version'] = 'RedMew has a version as well, accessible via /redmew-version',
|
||||
['color'] = 'RedMew allows color saving and a color randomiser: check out /redmew-color',
|
||||
['ban'] = 'In case your forgot: please remember to include a message on how to appeal a ban'
|
||||
}
|
||||
|
||||
local option_names = {
|
||||
['description'] = 'A description of the command',
|
||||
['arguments'] = 'A table of arguments, example: {"foo", "bar"} would map the first 2 arguments to foo and bar',
|
||||
['default_values'] = 'A default value for a given argument when omitted, example: {bar = false}',
|
||||
['required_rank'] = 'Set this to determines what rank is required to execute a command',
|
||||
['donator_only'] = 'Set this to true if only donators may execute this command',
|
||||
['debug_only'] = 'Set this to true if it should be registered when _DEBUG is true',
|
||||
['cheat_only'] = 'Set this to true if it should be registered when _CHEATS is true',
|
||||
['allowed_by_server'] = 'Set to true if the server (host) may execute this command',
|
||||
['allowed_by_player'] = 'Set to false to disable players from executing this command',
|
||||
['log_command'] = 'Set to true to log commands. Always true when admin is required',
|
||||
['capture_excess_arguments'] = 'Allows the last argument to be the remaining text in the command',
|
||||
['custom_help_text'] = 'Sets a custom help text to override the auto-generated help',
|
||||
}
|
||||
|
||||
---Validates if there aren't any wrong fields in the options.
|
||||
---@param command_name string
|
||||
---@param options table
|
||||
local function assert_existing_options(command_name, options)
|
||||
local invalid = {}
|
||||
for name, _ in pairs(options) do
|
||||
if not option_names[name] then
|
||||
insert(invalid, name)
|
||||
end
|
||||
end
|
||||
|
||||
if next(invalid) then
|
||||
error(format("The following options were given to the command '%s' but are invalid: %s", command_name, serialize(invalid)))
|
||||
end
|
||||
end
|
||||
|
||||
---Adds a command to be executed.
|
||||
---
|
||||
---Options table accepts the following structure: {
|
||||
--- description = 'A description of the command',
|
||||
--- arguments = {'foo', 'bar'}, -- maps arguments to these names in the given sequence
|
||||
--- default_values = {bar = false}, -- gives a default value to 'bar' when omitted
|
||||
--- required_rank = Ranks.regular, -- defaults to Ranks.guest
|
||||
--- donator_only = true, -- defaults to false
|
||||
--- debug_only = true, -- registers the command if _DEBUG is set to true, defaults to false
|
||||
--- cheat_only = true, -- registers the command if _CHEATS is set to true, defaults to false
|
||||
--- allowed_by_server = true, -- lets the server execute this, defaults to false
|
||||
--- allowed_by_player = false, -- lets players execute this, defaults to true
|
||||
--- log_command = true, -- defaults to false unless admin only, then always true
|
||||
--- capture_excess_arguments = true, -- defaults to false, captures excess arguments in the last argument, useful for sentences
|
||||
---}
|
||||
---
|
||||
---The callback receives the following arguments:
|
||||
--- - arguments (indexed by name, value is extracted from the parameters)
|
||||
--- - the LuaPlayer or nil if it doesn't exist (such as the server player)
|
||||
--- - the game tick in which the command was executed
|
||||
---
|
||||
---@param command_name string
|
||||
---@param options table
|
||||
---@param callback function
|
||||
function Command.add(command_name, options, callback)
|
||||
local description = options.description or '[Undocumented command]'
|
||||
local arguments = options.arguments or {}
|
||||
local default_values = options.default_values or {}
|
||||
local required_rank = options.required_rank or Ranks.guest
|
||||
local donator_only = options.donator_only or false
|
||||
local debug_only = options.debug_only or false
|
||||
local cheat_only = options.cheat_only or false
|
||||
local capture_excess_arguments = options.capture_excess_arguments or false
|
||||
local custom_help_text = options.custom_help_text or false
|
||||
local allowed_by_server = options.allowed_by_server or false
|
||||
local allowed_by_player = options.allowed_by_player
|
||||
local log_command = options.log_command or (required_rank >= Ranks.admin) or false
|
||||
local argument_list_size = table_size(arguments)
|
||||
local argument_list = ''
|
||||
|
||||
assert_existing_options(command_name, options)
|
||||
|
||||
if nil == options.allowed_by_player then
|
||||
allowed_by_player = true
|
||||
end
|
||||
|
||||
if (not _DEBUG and debug_only) and (not _CHEATS and cheat_only) then
|
||||
return
|
||||
end
|
||||
|
||||
if not allowed_by_player and not allowed_by_server then
|
||||
error(format("The command '%s' is not allowed by the server nor player, please enable at least one of them.", command_name))
|
||||
end
|
||||
|
||||
for index, argument_name in pairs(arguments) do
|
||||
local argument_display = argument_name
|
||||
for default_value_name, _ in pairs(default_values) do
|
||||
if default_value_name == argument_name then
|
||||
argument_display = argument_display .. ':optional'
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
if argument_list_size == index and capture_excess_arguments then
|
||||
argument_display = argument_display .. ':sentence'
|
||||
end
|
||||
|
||||
argument_list = format('%s<%s> ', argument_list, argument_display)
|
||||
end
|
||||
|
||||
local extra = ''
|
||||
|
||||
if allowed_by_server and not allowed_by_player then
|
||||
extra = ' (Server only)'
|
||||
elseif allowed_by_player and (required_rank > Ranks.guest) then
|
||||
extra = {'command.required_rank', get_rank_name(required_rank)}
|
||||
elseif allowed_by_player and donator_only then
|
||||
extra = ' (Donator only)'
|
||||
end
|
||||
|
||||
local help_text = {'command.help_text_format',(custom_help_text or argument_list), description, extra}
|
||||
|
||||
commands.add_command(command_name, help_text, function (command)
|
||||
local print -- custom print reference in case no player is present
|
||||
local player = game.player
|
||||
local player_name = player and player.valid and player.name or '<server>'
|
||||
if not player or not player.valid then
|
||||
print = log
|
||||
|
||||
if not allowed_by_server then
|
||||
print(format("The command '%s' is not allowed to be executed by the server.", command_name))
|
||||
return
|
||||
end
|
||||
else
|
||||
print = player.print
|
||||
|
||||
if not allowed_by_player then
|
||||
print(format("The command '%s' is not allowed to be executed by players.", command_name))
|
||||
return
|
||||
end
|
||||
|
||||
if Rank.less_than(player_name, required_rank) then
|
||||
print({'command.higher_rank_needed', command_name, get_rank_name(required_rank)})
|
||||
return
|
||||
end
|
||||
|
||||
if donator_only and not Donator.is_donator(player_name) then
|
||||
print(format("The command '%s' is only allowed for donators.", command_name))
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
local named_arguments = {}
|
||||
local from_command = {}
|
||||
local raw_parameter_index = 1
|
||||
for param in gmatch(command.parameter or '', '%S+') do
|
||||
if capture_excess_arguments and raw_parameter_index == argument_list_size then
|
||||
if not from_command[raw_parameter_index] then
|
||||
from_command[raw_parameter_index] = param
|
||||
else
|
||||
from_command[raw_parameter_index] = from_command[raw_parameter_index] .. ' ' .. param
|
||||
end
|
||||
else
|
||||
from_command[raw_parameter_index] = param
|
||||
raw_parameter_index = raw_parameter_index + 1
|
||||
end
|
||||
end
|
||||
|
||||
local errors = {}
|
||||
|
||||
for index, argument in pairs(arguments) do
|
||||
local parameter = from_command[index]
|
||||
|
||||
if not parameter then
|
||||
for default_value_name, default_value in pairs(default_values) do
|
||||
if default_value_name == argument then
|
||||
parameter = default_value
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if parameter == nil then
|
||||
insert(errors, format('Argument "%s" from command %s is missing.', argument, command_name))
|
||||
else
|
||||
named_arguments[argument] = parameter
|
||||
end
|
||||
end
|
||||
|
||||
local return_early = false
|
||||
|
||||
for _, error in pairs(errors) do
|
||||
return_early = true
|
||||
print(error)
|
||||
end
|
||||
|
||||
if return_early then
|
||||
return
|
||||
end
|
||||
|
||||
if log_command then
|
||||
local tick = 'pre-game'
|
||||
if game then
|
||||
tick = Utils.format_time(game.tick)
|
||||
end
|
||||
local server_time = Server.get_current_time()
|
||||
if server_time then
|
||||
server_time = format('(Server time: %s)', Timestamp.to_string(server_time))
|
||||
else
|
||||
server_time = ''
|
||||
end
|
||||
|
||||
log(format('%s(Map time: %s) [%s Command] %s, used: %s %s', server_time, tick, (options.required_rank >= Ranks.admin) and 'Admin' or 'Player', player_name, command_name, serialize(named_arguments)))
|
||||
end
|
||||
|
||||
local success, error = pcall(function ()
|
||||
callback(named_arguments, player, command.tick)
|
||||
end)
|
||||
|
||||
if not success then
|
||||
local serialized_arguments = serialize(named_arguments)
|
||||
if _DEBUG then
|
||||
print(format("%s triggered an error running a command and has been logged: '%s' with arguments %s", player_name, command_name, serialized_arguments))
|
||||
print(error)
|
||||
return
|
||||
end
|
||||
|
||||
print(format('There was an error running %s, it has been logged.', command_name))
|
||||
log(format("Error while running '%s' with arguments %s: %s", command_name, serialized_arguments, error))
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
function Command.search(keyword)
|
||||
local matches = {}
|
||||
local count = 0
|
||||
keyword = keyword:lower()
|
||||
for name, description in pairs(commands.commands) do
|
||||
local command = format('%s %s', name, description)
|
||||
if match(command:lower(), keyword) then
|
||||
count = count + 1
|
||||
matches[count] = command
|
||||
end
|
||||
end
|
||||
|
||||
-- built-in commands use LocalisedString, which cannot be translated until player.print is called
|
||||
for name in pairs(commands.game_commands) do
|
||||
name = name
|
||||
if match(name:lower(), keyword) then
|
||||
count = count + 1
|
||||
matches[count] = name
|
||||
end
|
||||
end
|
||||
|
||||
return matches
|
||||
end
|
||||
|
||||
--- Trigger messages on deprecated or defined commands, ignores the server
|
||||
local function on_command(event)
|
||||
if not event.player_index then
|
||||
return
|
||||
end
|
||||
|
||||
local alternative = deprecated_command_alternatives[event.command]
|
||||
if alternative then
|
||||
local player = Game.get_player_by_index(event.player_index)
|
||||
if player then
|
||||
player.print(format('Warning! Usage of the command "/%s" is deprecated. Please use "/%s" instead.', event.command, alternative))
|
||||
end
|
||||
end
|
||||
|
||||
local notification = notify_on_commands[event.command]
|
||||
if notification and event.player_index then
|
||||
local player = Game.get_player_by_index(event.player_index)
|
||||
if player then
|
||||
player.print(notification)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- Traps command errors if not in DEBUG.
|
||||
if not _DEBUG then
|
||||
local old_add_command = commands.add_command
|
||||
commands.add_command =
|
||||
function(name, desc, func)
|
||||
old_add_command(
|
||||
name,
|
||||
desc,
|
||||
function(cmd)
|
||||
local success, error = pcall(func, cmd)
|
||||
if not success then
|
||||
log(error)
|
||||
Game.player_print('Sorry there was an error running ' .. cmd.name)
|
||||
end
|
||||
end
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
Event.add(defines.events.on_console_command, on_command)
|
||||
|
||||
return Command
|
||||
223
utils/core.lua
223
utils/core.lua
@@ -1,223 +0,0 @@
|
||||
--- This file contains core utilities used by the redmew scenario.
|
||||
|
||||
-- Dependencies
|
||||
local Game = require 'utils.game' --- @dep utils.game
|
||||
local Color = require 'resources.color_presets' --- @dep resources.color_presets
|
||||
|
||||
-- localized functions
|
||||
local random = math.random
|
||||
local sqrt = math.sqrt
|
||||
local floor = math.floor
|
||||
local match = string.match
|
||||
local insert = table.insert
|
||||
local concat = table.concat
|
||||
|
||||
-- local constants
|
||||
local prefix = '## - '
|
||||
local minutes_to_ticks = 60 * 60
|
||||
local hours_to_ticks = 60 * 60 * 60
|
||||
local ticks_to_minutes = 1 / minutes_to_ticks
|
||||
local ticks_to_hours = 1 / hours_to_ticks
|
||||
|
||||
-- local variables
|
||||
local Module = {}
|
||||
|
||||
--- Measures distance between pos1 and pos2
|
||||
function Module.distance(pos1, pos2)
|
||||
local dx = pos2.x - pos1.x
|
||||
local dy = pos2.y - pos1.y
|
||||
return sqrt(dx * dx + dy * dy)
|
||||
end
|
||||
|
||||
--- Takes msg and prints it to all players except provided player
|
||||
-- @param msg <string|table> table if locale is used
|
||||
-- @param player <LuaPlayer> the player not to send the message to
|
||||
-- @param color <table> the color to use for the message, defaults to white
|
||||
function Module.print_except(msg, player, color)
|
||||
if not color then
|
||||
color = Color.white
|
||||
end
|
||||
|
||||
for _, p in pairs(game.connected_players) do
|
||||
if p ~= player then
|
||||
p.print(msg, color)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- Prints a message to all online admins
|
||||
-- @param msg <string|table> table if locale is used
|
||||
-- @param source <LuaPlayer|string|nil> string must be the name of a player, nil for server.
|
||||
function Module.print_admins(msg, source)
|
||||
local source_name
|
||||
local chat_color
|
||||
if source then
|
||||
if type(source) == 'string' then
|
||||
source_name = source
|
||||
chat_color = game.players[source].chat_color
|
||||
else
|
||||
source_name = source.name
|
||||
chat_color = source.chat_color
|
||||
end
|
||||
else
|
||||
source_name = 'Server'
|
||||
chat_color = Color.yellow
|
||||
end
|
||||
local formatted_msg = {'utils_core.print_admins',prefix, source_name, msg}
|
||||
log(formatted_msg)
|
||||
for _, p in pairs(game.connected_players) do
|
||||
if p.admin then
|
||||
p.print(formatted_msg, chat_color)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- Returns a valid string with the name of the actor of a command.
|
||||
function Module.get_actor()
|
||||
if game.player then
|
||||
return game.player.name
|
||||
end
|
||||
return '<server>'
|
||||
end
|
||||
|
||||
function Module.cast_bool(var)
|
||||
if var then
|
||||
return true
|
||||
else
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
function Module.find_entities_by_last_user(player, surface, filters)
|
||||
if type(player) == 'string' or not player then
|
||||
error("bad argument #1 to '" .. debug.getinfo(1, 'n').name .. "' (number or LuaPlayer expected, got " .. type(player) .. ')', 1)
|
||||
return
|
||||
end
|
||||
if type(surface) ~= 'table' and type(surface) ~= 'number' then
|
||||
error("bad argument #2 to '" .. debug.getinfo(1, 'n').name .. "' (number or LuaSurface expected, got " .. type(surface) .. ')', 1)
|
||||
return
|
||||
end
|
||||
local entities = {}
|
||||
local filter = filters or {}
|
||||
if type(surface) == 'number' then
|
||||
surface = game.surfaces[surface]
|
||||
end
|
||||
if type(player) == 'number' then
|
||||
player = Game.get_player_by_index(player)
|
||||
end
|
||||
filter.force = player.force.name
|
||||
for _, e in pairs(surface.find_entities_filtered(filter)) do
|
||||
if e.last_user == player then
|
||||
insert(entities, e)
|
||||
end
|
||||
end
|
||||
return entities
|
||||
end
|
||||
|
||||
function Module.ternary(c, t, f)
|
||||
if c then
|
||||
return t
|
||||
else
|
||||
return f
|
||||
end
|
||||
end
|
||||
|
||||
--- Takes a time in ticks and returns a string with the time in format "x hour(s) x minute(s)"
|
||||
function Module.format_time(ticks)
|
||||
local result = {}
|
||||
|
||||
local hours = floor(ticks * ticks_to_hours)
|
||||
if hours > 0 then
|
||||
ticks = ticks - hours * hours_to_ticks
|
||||
insert(result, hours)
|
||||
if hours == 1 then
|
||||
insert(result, 'hour')
|
||||
else
|
||||
insert(result, 'hours')
|
||||
end
|
||||
end
|
||||
|
||||
local minutes = floor(ticks * ticks_to_minutes)
|
||||
insert(result, minutes)
|
||||
if minutes == 1 then
|
||||
insert(result, 'minute')
|
||||
else
|
||||
insert(result, 'minutes')
|
||||
end
|
||||
|
||||
return concat(result, ' ')
|
||||
end
|
||||
|
||||
--- Prints a message letting the player know they cannot run a command
|
||||
-- @param name string name of the command
|
||||
function Module.cant_run(name)
|
||||
Game.player_print("Can't run command (" .. name .. ') - insufficient permission.')
|
||||
end
|
||||
|
||||
--- Logs the use of a command and its user
|
||||
-- @param actor string with the actor's name (usually acquired by calling get_actor)
|
||||
-- @param command the command's name as table element
|
||||
-- @param parameters the command's parameters as a table (optional)
|
||||
function Module.log_command(actor, command, parameters)
|
||||
local action = concat {'[Admin-Command] ', actor, ' used: ', command}
|
||||
if parameters then
|
||||
action = concat {action, ' ', parameters}
|
||||
end
|
||||
log(action)
|
||||
end
|
||||
|
||||
function Module.comma_value(n) -- credit http://richard.warburton.it
|
||||
local left, num, right = match(n, '^([^%d]*%d)(%d*)(.-)$')
|
||||
return left .. (num:reverse():gsub('(%d%d%d)', '%1,'):reverse()) .. right
|
||||
end
|
||||
|
||||
--- Asserts the argument is one of type arg_types
|
||||
-- @param arg the variable to check
|
||||
-- @param arg_types the type as a table of sings
|
||||
-- @return boolean
|
||||
function Module.verify_mult_types(arg, arg_types)
|
||||
for _, arg_type in pairs(arg_types) do
|
||||
if type(arg) == arg_type then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
--- Returns a random RGB color as a table
|
||||
function Module.random_RGB()
|
||||
return {r = random(0, 255), g = random(0, 255), b = random(0, 255)}
|
||||
end
|
||||
|
||||
--- Sets a table element to value while also returning value.
|
||||
-- @param tbl table to change the element of
|
||||
-- @param key string
|
||||
-- @param value nil|boolean|number|string|table to set the element to
|
||||
-- @return value
|
||||
function Module.set_and_return(tbl, key, value)
|
||||
tbl[key] = value
|
||||
return value
|
||||
end
|
||||
|
||||
-- add utility functions that exist in base factorio/util
|
||||
require 'util'
|
||||
|
||||
--- Moves a position according to the parameters given
|
||||
-- Notice: only accepts cardinal directions as direction
|
||||
-- @param position <table> table containing a map position
|
||||
-- @param direction <defines.direction> north, east, south, west
|
||||
-- @param distance <number>
|
||||
-- @return <table> modified position
|
||||
Module.move_position = util.moveposition
|
||||
|
||||
--- Takes a direction and gives you the opposite
|
||||
-- @param direction <defines.direction> north, east, south, west, northeast, northwest, southeast, southwest
|
||||
-- @return <number> representing the direction
|
||||
Module.opposite_direction = util.oppositedirection
|
||||
|
||||
--- Takes the string of a module and returns whether is it available or not
|
||||
-- @param name <string> the name of the module (ex. 'utils.core')
|
||||
-- @return <boolean>
|
||||
Module.is_module_available = util.ismoduleavailable
|
||||
|
||||
return Module
|
||||
164
utils/debug.lua
164
utils/debug.lua
@@ -1,164 +0,0 @@
|
||||
-- localised functions
|
||||
local format = string.format
|
||||
local match = string.match
|
||||
local gsub = string.gsub
|
||||
local serialize = serpent.line
|
||||
local debug_getupvalue = debug.getupvalue
|
||||
|
||||
-- this
|
||||
local Debug = {}
|
||||
|
||||
global.debug_message_count = 0
|
||||
|
||||
---@return number next index
|
||||
local function increment()
|
||||
local next = global.debug_message_count + 1
|
||||
global.debug_message_count = next
|
||||
|
||||
return next
|
||||
end
|
||||
|
||||
--- Takes the table output from debug.getinfo and pretties it
|
||||
local function cleanup_debug(debug_table)
|
||||
local short_src = match(debug_table.source, '/[^/]*/[^/]*$')
|
||||
-- require will not return a valid string so short_src may be nil here
|
||||
if short_src then
|
||||
short_src = gsub(short_src, '%.lua', '')
|
||||
end
|
||||
|
||||
return format('[function: %s file: %s line number: %s]', debug_table.name, short_src, debug_table.currentline)
|
||||
end
|
||||
|
||||
---Shows the given message if debug is enabled. Uses serpent to print non scalars.
|
||||
-- @param message <table|string|number|boolean>
|
||||
-- @param trace_levels <number|nil> levels of stack trace to give, defaults to 1 level if nil
|
||||
function Debug.print(message, trace_levels)
|
||||
if not _DEBUG then
|
||||
return
|
||||
end
|
||||
|
||||
if not trace_levels then
|
||||
trace_levels = 2
|
||||
else
|
||||
trace_levels = trace_levels + 1
|
||||
end
|
||||
|
||||
local traceback_string = ''
|
||||
if type(message) ~= 'string' and type(message) ~= 'number' and type(message) ~= 'boolean' then
|
||||
message = serialize(message)
|
||||
end
|
||||
|
||||
message = format('[%d] %s', increment(), tostring(message))
|
||||
|
||||
if trace_levels >= 2 then
|
||||
for i = 2, trace_levels do
|
||||
local debug_table = debug.getinfo(i)
|
||||
if debug_table then
|
||||
traceback_string = format('%s -> %s', traceback_string, cleanup_debug(debug_table))
|
||||
else
|
||||
break
|
||||
end
|
||||
end
|
||||
message = format('%s - Traceback%s', message, traceback_string)
|
||||
end
|
||||
|
||||
if _LIFECYCLE == _STAGE.runtime then
|
||||
game.print(message)
|
||||
end
|
||||
log(message)
|
||||
end
|
||||
|
||||
local function get(obj, prop)
|
||||
return obj[prop]
|
||||
end
|
||||
|
||||
local function get_lua_object_type_safe(obj)
|
||||
local s, r = pcall(get, obj, 'help')
|
||||
|
||||
if not s then
|
||||
return
|
||||
end
|
||||
|
||||
return r():match('Lua%a+')
|
||||
end
|
||||
|
||||
--- Returns the value of the key inside the object
|
||||
-- or 'InvalidLuaObject' if the LuaObject is invalid.
|
||||
-- or 'InvalidLuaObjectKey' if the LuaObject does not have an entry at that key
|
||||
-- @param object <table> LuaObject or metatable
|
||||
-- @param key <string>
|
||||
-- @return <any>
|
||||
function Debug.get_meta_value(object, key)
|
||||
if Debug.object_type(object) == 'InvalidLuaObject' then
|
||||
return 'InvalidLuaObject'
|
||||
end
|
||||
|
||||
local suc, value = pcall(get, object, key)
|
||||
if not suc then
|
||||
return 'InvalidLuaObjectKey'
|
||||
end
|
||||
|
||||
return value
|
||||
end
|
||||
|
||||
--- Returns the Lua data type or the factorio LuaObject type
|
||||
-- or 'NoHelpLuaObject' if the LuaObject does not have a help function
|
||||
-- or 'InvalidLuaObject' if the LuaObject is invalid.
|
||||
-- @param object <any>
|
||||
-- @return string
|
||||
function Debug.object_type(object)
|
||||
local obj_type = type(object)
|
||||
|
||||
if obj_type ~= 'table' or type(object.__self) ~= 'userdata' then
|
||||
return obj_type
|
||||
end
|
||||
|
||||
local suc, valid = pcall(get, object, 'valid')
|
||||
if not suc then
|
||||
-- no 'valid' property
|
||||
return get_lua_object_type_safe(object) or 'NoHelpLuaObject'
|
||||
end
|
||||
|
||||
if not valid then
|
||||
return 'InvalidLuaObject'
|
||||
else
|
||||
return get_lua_object_type_safe(object) or 'NoHelpLuaObject'
|
||||
end
|
||||
end
|
||||
|
||||
---Shows the given message if debug is on.
|
||||
---@param position Position
|
||||
---@param message string
|
||||
function Debug.print_position(position, message)
|
||||
Debug.print(format('%s %s', serialize(position), message))
|
||||
end
|
||||
|
||||
---Executes the given callback if cheating is enabled.
|
||||
---@param callback function
|
||||
function Debug.cheat(callback)
|
||||
if _CHEATS then
|
||||
callback()
|
||||
end
|
||||
end
|
||||
|
||||
--- Returns true if the function is a closure, false otherwise.
|
||||
-- A closure is a function that contains 'upvalues' or in other words
|
||||
-- has a reference to a local variable defined outside the function's scope.
|
||||
-- @param func<function>
|
||||
-- @return boolean
|
||||
function Debug.is_closure(func)
|
||||
local i = 1
|
||||
while true do
|
||||
local n = debug_getupvalue(func, i)
|
||||
|
||||
if n == nil then
|
||||
return false
|
||||
elseif n ~= '_ENV' then
|
||||
return true
|
||||
end
|
||||
|
||||
i = i + 1
|
||||
end
|
||||
end
|
||||
|
||||
return Debug
|
||||
@@ -1,32 +0,0 @@
|
||||
--- A small debugging tool that writes the contents of _ENV to a file when the game loads.
|
||||
-- Useful for ensuring you get the same information when loading
|
||||
-- the reference and desync levels in desync reports.
|
||||
-- dependencies
|
||||
local table = require 'utils.table' --- @dep utils.table
|
||||
local Event = require 'utils.event' --- @dep utils.event
|
||||
|
||||
-- localized functions
|
||||
local inspect = table.inspect
|
||||
|
||||
-- local constants
|
||||
local filename = 'env_dump.lua'
|
||||
|
||||
-- Removes metatables and the package table
|
||||
local filter = function(item, path)
|
||||
if path[#path] ~= inspect.METATABLE and item ~= 'package' then
|
||||
return item
|
||||
end
|
||||
end
|
||||
|
||||
local function player_joined(event)
|
||||
local dump_string = inspect(_ENV, {process = filter})
|
||||
if dump_string then
|
||||
local s = string.format('tick on join: %s\n%s', event.tick, dump_string)
|
||||
game.write_file(filename, s)
|
||||
game.print('_ENV dumped into ' .. filename)
|
||||
else
|
||||
game.print('_ENV not dumped, dump_string was nil')
|
||||
end
|
||||
end
|
||||
|
||||
Event.add(defines.events.on_player_joined_game, player_joined)
|
||||
@@ -97,7 +97,7 @@
|
||||
local EventCore = require 'utils.event_core' --- @dep utils.event_core
|
||||
local Global = require 'utils.global' --- @dep utils.global
|
||||
local Token = require 'utils.token' --- @dep utils.token
|
||||
local Debug = require 'utils.debug' --- @dep utils.debug
|
||||
local Debug = require 'overrides.debug' --- @dep overrides.debug
|
||||
|
||||
local table_remove = table.remove
|
||||
local core_add = EventCore.add
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
local Global = require 'utils.global' --- @dep utils.global
|
||||
local Color = require 'resources.color_presets' --- @dep resources.color_presets
|
||||
local Color = require 'utils.color_presets' --- @dep utils.color_presets
|
||||
local pairs = pairs
|
||||
|
||||
local Game = {}
|
||||
|
||||
@@ -1,342 +0,0 @@
|
||||
local inspect ={
|
||||
_VERSION = 'inspect.lua 3.1.0',
|
||||
_URL = 'http://github.com/kikito/inspect.lua',
|
||||
_DESCRIPTION = 'human-readable representations of tables',
|
||||
_LICENSE = [[
|
||||
MIT LICENSE
|
||||
|
||||
Copyright (c) 2013 Enrique García Cota
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included
|
||||
in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
]]
|
||||
}
|
||||
|
||||
local tostring = tostring
|
||||
|
||||
inspect.KEY = setmetatable({}, {__tostring = function() return 'inspect.KEY' end})
|
||||
inspect.METATABLE = setmetatable({}, {__tostring = function() return 'inspect.METATABLE' end})
|
||||
|
||||
-- Apostrophizes the string if it has quotes, but not aphostrophes
|
||||
-- Otherwise, it returns a regular quoted string
|
||||
local function smartQuote(str)
|
||||
if str:match('"') and not str:match("'") then
|
||||
return "'" .. str .. "'"
|
||||
end
|
||||
return '"' .. str:gsub('"', '\\"') .. '"'
|
||||
end
|
||||
|
||||
-- \a => '\\a', \0 => '\\0', 31 => '\31'
|
||||
local shortControlCharEscapes = {
|
||||
["\a"] = "\\a", ["\b"] = "\\b", ["\f"] = "\\f", ["\n"] = "\\n",
|
||||
["\r"] = "\\r", ["\t"] = "\\t", ["\v"] = "\\v"
|
||||
}
|
||||
local longControlCharEscapes = {} -- \a => nil, \0 => \000, 31 => \031
|
||||
for i=0, 31 do
|
||||
local ch = string.char(i)
|
||||
if not shortControlCharEscapes[ch] then
|
||||
shortControlCharEscapes[ch] = "\\"..i
|
||||
longControlCharEscapes[ch] = string.format("\\%03d", i)
|
||||
end
|
||||
end
|
||||
|
||||
local function escape(str)
|
||||
return (str:gsub("\\", "\\\\")
|
||||
:gsub("(%c)%f[0-9]", longControlCharEscapes)
|
||||
:gsub("%c", shortControlCharEscapes))
|
||||
end
|
||||
|
||||
local function isIdentifier(str)
|
||||
return type(str) == 'string' and str:match( "^[_%a][_%a%d]*$" )
|
||||
end
|
||||
|
||||
local function isSequenceKey(k, sequenceLength)
|
||||
return type(k) == 'number'
|
||||
and 1 <= k
|
||||
and k <= sequenceLength
|
||||
and math.floor(k) == k
|
||||
end
|
||||
|
||||
local defaultTypeOrders = {
|
||||
['number'] = 1, ['boolean'] = 2, ['string'] = 3, ['table'] = 4,
|
||||
['function'] = 5, ['userdata'] = 6, ['thread'] = 7
|
||||
}
|
||||
|
||||
local function sortKeys(a, b)
|
||||
local ta, tb = type(a), type(b)
|
||||
|
||||
-- strings and numbers are sorted numerically/alphabetically
|
||||
if ta == tb and (ta == 'string' or ta == 'number') then return a < b end
|
||||
|
||||
local dta, dtb = defaultTypeOrders[ta], defaultTypeOrders[tb]
|
||||
-- Two default types are compared according to the defaultTypeOrders table
|
||||
if dta and dtb then return defaultTypeOrders[ta] < defaultTypeOrders[tb]
|
||||
elseif dta then return true -- default types before custom ones
|
||||
elseif dtb then return false -- custom types after default ones
|
||||
end
|
||||
|
||||
-- custom types are sorted out alphabetically
|
||||
return ta < tb
|
||||
end
|
||||
|
||||
-- For implementation reasons, the behavior of rawlen & # is "undefined" when
|
||||
-- tables aren't pure sequences. So we implement our own # operator.
|
||||
local function getSequenceLength(t)
|
||||
local len = 1
|
||||
local v = rawget(t,len)
|
||||
while v ~= nil do
|
||||
len = len + 1
|
||||
v = rawget(t,len)
|
||||
end
|
||||
return len - 1
|
||||
end
|
||||
|
||||
local function getNonSequentialKeys(t)
|
||||
local keys = {}
|
||||
local sequenceLength = getSequenceLength(t)
|
||||
for k,_ in pairs(t) do
|
||||
if not isSequenceKey(k, sequenceLength) then table.insert(keys, k) end
|
||||
end
|
||||
table.sort(keys, sortKeys)
|
||||
return keys, sequenceLength
|
||||
end
|
||||
|
||||
local function getToStringResultSafely(t, mt)
|
||||
local __tostring = type(mt) == 'table' and rawget(mt, '__tostring')
|
||||
local str, ok
|
||||
if type(__tostring) == 'function' then
|
||||
ok, str = pcall(__tostring, t)
|
||||
str = ok and str or 'error: ' .. tostring(str)
|
||||
end
|
||||
if type(str) == 'string' and #str > 0 then return str end
|
||||
end
|
||||
|
||||
local function countTableAppearances(t, tableAppearances)
|
||||
tableAppearances = tableAppearances or {}
|
||||
|
||||
if type(t) == 'table' then
|
||||
if not tableAppearances[t] then
|
||||
tableAppearances[t] = 1
|
||||
for k,v in pairs(t) do
|
||||
countTableAppearances(k, tableAppearances)
|
||||
countTableAppearances(v, tableAppearances)
|
||||
end
|
||||
countTableAppearances(getmetatable(t), tableAppearances)
|
||||
else
|
||||
tableAppearances[t] = tableAppearances[t] + 1
|
||||
end
|
||||
end
|
||||
|
||||
return tableAppearances
|
||||
end
|
||||
|
||||
local copySequence = function(s)
|
||||
local copy, len = {}, #s
|
||||
for i=1, len do copy[i] = s[i] end
|
||||
return copy, len
|
||||
end
|
||||
|
||||
local function makePath(path, ...)
|
||||
local keys = {...}
|
||||
local newPath, len = copySequence(path)
|
||||
for i=1, #keys do
|
||||
newPath[len + i] = keys[i]
|
||||
end
|
||||
return newPath
|
||||
end
|
||||
|
||||
local function processRecursive(process, item, path, visited)
|
||||
|
||||
if item == nil then return nil end
|
||||
if visited[item] then return visited[item] end
|
||||
|
||||
local processed = process(item, path)
|
||||
if type(processed) == 'table' then
|
||||
local processedCopy = {}
|
||||
visited[item] = processedCopy
|
||||
local processedKey
|
||||
|
||||
for k,v in pairs(processed) do
|
||||
processedKey = processRecursive(process, k, makePath(path, k, inspect.KEY), visited)
|
||||
if processedKey ~= nil then
|
||||
processedCopy[processedKey] = processRecursive(process, v, makePath(path, processedKey), visited)
|
||||
end
|
||||
end
|
||||
|
||||
local mt = processRecursive(process, getmetatable(processed), makePath(path, inspect.METATABLE), visited)
|
||||
setmetatable(processedCopy, mt)
|
||||
processed = processedCopy
|
||||
end
|
||||
return processed
|
||||
end
|
||||
|
||||
|
||||
|
||||
-------------------------------------------------------------------
|
||||
|
||||
local Inspector = {}
|
||||
local Inspector_mt = {__index = Inspector}
|
||||
|
||||
function Inspector:puts(...)
|
||||
local args = {...}
|
||||
local buffer = self.buffer
|
||||
local len = #buffer
|
||||
for i=1, #args do
|
||||
len = len + 1
|
||||
buffer[len] = args[i]
|
||||
end
|
||||
end
|
||||
|
||||
function Inspector:down(f)
|
||||
self.level = self.level + 1
|
||||
f()
|
||||
self.level = self.level - 1
|
||||
end
|
||||
|
||||
function Inspector:tabify()
|
||||
self:puts(self.newline, string.rep(self.indent, self.level))
|
||||
end
|
||||
|
||||
function Inspector:alreadyVisited(v)
|
||||
return self.ids[v] ~= nil
|
||||
end
|
||||
|
||||
function Inspector:getId(v)
|
||||
local id = self.ids[v]
|
||||
if not id then
|
||||
local tv = type(v)
|
||||
id = (self.maxIds[tv] or 0) + 1
|
||||
self.maxIds[tv] = id
|
||||
self.ids[v] = id
|
||||
end
|
||||
return tostring(id)
|
||||
end
|
||||
|
||||
function Inspector:putKey(k)
|
||||
if isIdentifier(k) then return self:puts(k) end
|
||||
self:puts("[")
|
||||
self:putValue(k)
|
||||
self:puts("]")
|
||||
end
|
||||
|
||||
function Inspector:putTable(t)
|
||||
if t == inspect.KEY or t == inspect.METATABLE then
|
||||
self:puts(tostring(t))
|
||||
elseif self:alreadyVisited(t) then
|
||||
self:puts('<table ', self:getId(t), '>')
|
||||
elseif self.level >= self.depth then
|
||||
self:puts('{...}')
|
||||
else
|
||||
if self.tableAppearances[t] > 1 then self:puts('<', self:getId(t), '>') end
|
||||
|
||||
local nonSequentialKeys, sequenceLength = getNonSequentialKeys(t)
|
||||
local mt = getmetatable(t)
|
||||
local toStringResult = getToStringResultSafely(t, mt)
|
||||
|
||||
self:puts('{')
|
||||
self:down(function()
|
||||
if toStringResult then
|
||||
self:puts(' -- ', escape(toStringResult))
|
||||
if sequenceLength >= 1 then self:tabify() end
|
||||
end
|
||||
|
||||
local count = 0
|
||||
for i=1, sequenceLength do
|
||||
if count > 0 then self:puts(',') end
|
||||
self:puts(' ')
|
||||
self:putValue(t[i])
|
||||
count = count + 1
|
||||
end
|
||||
|
||||
for _,k in ipairs(nonSequentialKeys) do
|
||||
if count > 0 then self:puts(',') end
|
||||
self:tabify()
|
||||
self:putKey(k)
|
||||
self:puts(' = ')
|
||||
self:putValue(t[k])
|
||||
count = count + 1
|
||||
end
|
||||
|
||||
if mt then
|
||||
if count > 0 then self:puts(',') end
|
||||
self:tabify()
|
||||
self:puts('<metatable> = ')
|
||||
self:putValue(mt)
|
||||
end
|
||||
end)
|
||||
|
||||
if #nonSequentialKeys > 0 or mt then -- result is multi-lined. Justify closing }
|
||||
self:tabify()
|
||||
elseif sequenceLength > 0 then -- array tables have one extra space before closing }
|
||||
self:puts(' ')
|
||||
end
|
||||
|
||||
self:puts('}')
|
||||
end
|
||||
end
|
||||
|
||||
function Inspector:putValue(v)
|
||||
local tv = type(v)
|
||||
|
||||
if tv == 'string' then
|
||||
self:puts(smartQuote(escape(v)))
|
||||
elseif tv == 'number' or tv == 'boolean' or tv == 'nil' or
|
||||
tv == 'cdata' or tv == 'ctype' then
|
||||
self:puts(tostring(v))
|
||||
elseif tv == 'table' then
|
||||
self:putTable(v)
|
||||
else
|
||||
self:puts('<',tv,' ',self:getId(v),'>')
|
||||
end
|
||||
end
|
||||
|
||||
-------------------------------------------------------------------
|
||||
|
||||
function inspect.inspect(root, options)
|
||||
options = options or {}
|
||||
|
||||
local depth = options.depth or math.huge
|
||||
local newline = options.newline or '\n'
|
||||
local indent = options.indent or ' '
|
||||
local process = options.process
|
||||
|
||||
if process then
|
||||
root = processRecursive(process, root, {}, {})
|
||||
end
|
||||
|
||||
local inspector = setmetatable({
|
||||
depth = depth,
|
||||
level = 0,
|
||||
buffer = {},
|
||||
ids = {},
|
||||
maxIds = {},
|
||||
newline = newline,
|
||||
indent = indent,
|
||||
tableAppearances = countTableAppearances(root)
|
||||
}, Inspector_mt)
|
||||
|
||||
inspector:putValue(root)
|
||||
|
||||
return table.concat(inspector.buffer)
|
||||
end
|
||||
|
||||
setmetatable(inspect, { __call = function(_, ...) return inspect.inspect(...) end })
|
||||
|
||||
return inspect
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
--luacheck:ignore global math
|
||||
local _sin = math.sin
|
||||
local _cos = math.cos
|
||||
|
||||
math.sqrt2 = math.sqrt(2)
|
||||
math.inv_sqrt2 = 1 / math.sqrt2
|
||||
math.tau = 2 * math.pi
|
||||
|
||||
math.sin = function(x)
|
||||
return math.floor(_sin(x) * 10000000 + 0.5) / 10000000
|
||||
end
|
||||
|
||||
math.cos = function(x)
|
||||
return math.floor(_cos(x) * 10000000 + 0.5) / 10000000
|
||||
end
|
||||
|
||||
-- rounds number (num) to certain number of decimal places (idp)
|
||||
math.round = function(num, idp)
|
||||
local mult = 10 ^ (idp or 0)
|
||||
return math.floor(num * mult + 0.5) / mult
|
||||
end
|
||||
|
||||
math.clamp = function(num, min, max)
|
||||
if num < min then
|
||||
return min
|
||||
elseif num > max then
|
||||
return max
|
||||
else
|
||||
return num
|
||||
end
|
||||
end
|
||||
|
||||
--- Takes two points and calculates the slope of a line
|
||||
-- @param x1, y1 numbers - coordinates of a point on a line
|
||||
-- @param x2, y2 numbers - coordinates of a point on a line
|
||||
-- @return number - the slope of the line
|
||||
math.calculate_slope = function(x1, y1, x2, y2)
|
||||
return math.abs((y2 - y1) / (x2 - x1))
|
||||
end
|
||||
|
||||
--- Calculates the y-intercept of a line
|
||||
-- @param x number - coordinates of point on line
|
||||
-- @param y number - coordinates of point on line
|
||||
-- @param slope number - the slope of a line
|
||||
-- @return number - the y-intercept of a line
|
||||
math.calculate_y_intercept = function(x, y, slope)
|
||||
return y - (slope * x)
|
||||
end
|
||||
|
||||
local deg_to_rad = math.tau / 360
|
||||
math.degrees = function(angle)
|
||||
return angle * deg_to_rad
|
||||
end
|
||||
|
||||
return math
|
||||
@@ -1,154 +0,0 @@
|
||||
local Global = require 'utils.global' --- @dep utils.global
|
||||
local Game = require 'utils.game' --- @dep utils.game
|
||||
local PlayerStats = require 'features.player_stats' --- @dep features.player_stats
|
||||
local Command = require 'utils.command' --- @dep utils.command
|
||||
local Ranks = require 'resources.ranks' --- @dep resources.ranks
|
||||
|
||||
local format = string.format
|
||||
local abs = math.abs
|
||||
local concat = table.concat
|
||||
|
||||
local Public = {}
|
||||
local reward_token = {global.config.player_rewards.token} or {global.config.market.currency} or {'coin'}
|
||||
|
||||
Global.register(
|
||||
{
|
||||
reward_token = reward_token
|
||||
},
|
||||
function(tbl)
|
||||
reward_token = tbl.reward_token
|
||||
end
|
||||
)
|
||||
|
||||
--- Returns the single or plural form of the token name
|
||||
local function get_token_plural(quantity)
|
||||
if quantity and quantity > 1 then
|
||||
return concat({reward_token[1], 's'})
|
||||
else
|
||||
return reward_token[1]
|
||||
end
|
||||
end
|
||||
|
||||
--- Set the item to use for rewards
|
||||
-- @param reward string - item name to use as reward
|
||||
-- @return boolean true - indicating success
|
||||
Public.set_reward = function(reward)
|
||||
if global.config.player_rewards.enabled == false then
|
||||
return false
|
||||
end
|
||||
|
||||
reward_token[1] = reward
|
||||
return true
|
||||
end
|
||||
|
||||
--- Returns the name of the reward item
|
||||
Public.get_reward = function()
|
||||
return reward_token[1]
|
||||
end
|
||||
|
||||
--- Gives reward tokens to the player
|
||||
-- @param player <number|LuaPlayer>
|
||||
-- @param amount <number> of reward tokens
|
||||
-- @param message <string> an optional message to send to the affected player
|
||||
-- @return <number> indicating how many were inserted or if operation failed
|
||||
Public.give_reward = function(player, amount, message)
|
||||
if global.config.player_rewards.enabled == false then
|
||||
return 0
|
||||
end
|
||||
|
||||
local player_index
|
||||
if type(player) == 'number' then
|
||||
player_index = player
|
||||
player = Game.get_player_by_index(player)
|
||||
else
|
||||
player_index = player.index
|
||||
end
|
||||
local reward = {name = reward_token[1], count = amount}
|
||||
if not player.can_insert(reward) then
|
||||
return 0
|
||||
end
|
||||
if message then
|
||||
player.print(message)
|
||||
end
|
||||
local coin_difference = player.insert(reward)
|
||||
if reward_token[1] == 'coin' then
|
||||
PlayerStats.change_coin_earned(player_index, coin_difference)
|
||||
end
|
||||
return coin_difference
|
||||
end
|
||||
|
||||
--- Removes reward tokens from the player
|
||||
-- @param player <number|LuaPlayer>
|
||||
-- @param amount <number> of reward tokens
|
||||
-- @param message <string> an optional message to send to the affected player
|
||||
-- @return <number> indicating how many were removed or if operation failed
|
||||
Public.remove_reward = function(player, amount, message)
|
||||
if global.config.player_rewards.enabled == false then
|
||||
return 0
|
||||
end
|
||||
|
||||
local player_index
|
||||
if type(player) == 'number' then
|
||||
player_index = player
|
||||
player = Game.get_player_by_index(player)
|
||||
else
|
||||
player_index = player.index
|
||||
end
|
||||
local unreward = {name = reward_token[1], count = amount}
|
||||
if message then
|
||||
player.print(message)
|
||||
end
|
||||
local coin_difference = player.remove_item(unreward)
|
||||
if reward_token[1] == 'coin' then
|
||||
PlayerStats.change_coin_earned(player_index, -coin_difference)
|
||||
end
|
||||
return coin_difference
|
||||
end
|
||||
|
||||
Command.add(
|
||||
'reward',
|
||||
{
|
||||
description = 'Gives a reward to a target player (removes if quantity is negative)',
|
||||
arguments = {'target', 'quantity', 'reason'},
|
||||
default_values = {reason = false},
|
||||
required_rank = Ranks.admin,
|
||||
capture_excess_arguments = true,
|
||||
allowed_by_server = true,
|
||||
allowed_by_player = true
|
||||
},
|
||||
function(args, player)
|
||||
local player_name = 'server'
|
||||
if player then
|
||||
player_name = player.name
|
||||
end
|
||||
|
||||
local target_name = args.target
|
||||
local target = game.players[target_name]
|
||||
if not target then
|
||||
player.print('Target not found.')
|
||||
return
|
||||
end
|
||||
|
||||
local quantity = tonumber(args.quantity)
|
||||
if quantity > 0 then
|
||||
Public.give_reward(target, quantity)
|
||||
local string = format('%s has rewarded %s with %s %s', player_name, target_name, quantity, get_token_plural(quantity))
|
||||
if args.reason then
|
||||
string = format('%s for %s', string, args.reason)
|
||||
end
|
||||
game.print(string)
|
||||
elseif quantity < 0 then
|
||||
quantity = abs(quantity)
|
||||
Public.remove_reward(target, quantity)
|
||||
local string = format('%s has punished %s by taking %s %s', player_name, target_name, quantity, get_token_plural(quantity))
|
||||
if args.reason then
|
||||
string = format('%s for %s', string, args.reason)
|
||||
end
|
||||
game.print(string)
|
||||
else
|
||||
Game.player_print(" A reward of 0 is neither a reward nor a punishment, it's just dumb. Try harder.")
|
||||
end
|
||||
end
|
||||
)
|
||||
|
||||
return Public
|
||||
@@ -1,13 +0,0 @@
|
||||
local Public = {}
|
||||
|
||||
local locale_string = {'', '[PRINT] ', nil}
|
||||
local raw_print = print
|
||||
|
||||
function print(str)
|
||||
locale_string[3] = str
|
||||
log(locale_string)
|
||||
end
|
||||
|
||||
Public.raw_print = raw_print
|
||||
|
||||
return Public
|
||||
@@ -1,58 +0,0 @@
|
||||
--- A module to prevent recipes from being unlocked by research. Accessed via the public functions.
|
||||
local Event = require 'utils.event' --- @dep utils.event
|
||||
local Global = require 'utils.global' --- @dep utils.global
|
||||
|
||||
local Public = {}
|
||||
|
||||
local recipes = {}
|
||||
|
||||
Global.register(
|
||||
{
|
||||
recipes = recipes
|
||||
},
|
||||
function(tbl)
|
||||
recipes = tbl.recipes
|
||||
end
|
||||
)
|
||||
|
||||
Event.add(
|
||||
defines.events.on_research_finished,
|
||||
function(event)
|
||||
local p_force = game.forces.player
|
||||
local r = event.research
|
||||
for _, effect in pairs(r.effects) do
|
||||
local recipe = effect.recipe
|
||||
if recipe and recipes[recipe] then
|
||||
p_force.recipes[recipe].enabled = false
|
||||
end
|
||||
end
|
||||
end
|
||||
)
|
||||
|
||||
Event.on_init(
|
||||
function()
|
||||
for recipe in pairs(recipes) do
|
||||
game.forces.player.recipes[recipe].enabled = false
|
||||
end
|
||||
end
|
||||
)
|
||||
|
||||
--- Locks recipes, preventing them from being enabled by research.
|
||||
-- Does not check if they should be enabled/disabled by existing research.
|
||||
-- @param tbl <table> an array of recipe strings
|
||||
function Public.lock_recipes(tbl)
|
||||
for i = 1, #tbl do
|
||||
recipes[tbl[i]] = true
|
||||
end
|
||||
end
|
||||
|
||||
--- Unlocks recipes, allowing them to be enabled by research.
|
||||
-- Does not check if they should be enabled/disabled by existing research.
|
||||
-- @param tbl <table> an array of recipe strings
|
||||
function Public.unlock_recipes(tbl)
|
||||
for i = 1, #tbl do
|
||||
recipes[tbl[i]] = nil
|
||||
end
|
||||
end
|
||||
|
||||
return Public
|
||||
@@ -1,178 +0,0 @@
|
||||
local Global = require 'utils.global' --- @dep utils.global
|
||||
local type = type
|
||||
local error = error
|
||||
local tonumber = tonumber
|
||||
local tostring = tostring
|
||||
local pairs = pairs
|
||||
local format = string.format
|
||||
|
||||
--- Contains a set of callable that will attempt to sanitize and transform the input
|
||||
local settings_type = {
|
||||
fraction = function (input)
|
||||
input = tonumber(input)
|
||||
|
||||
if input == nil then
|
||||
return false, 'fraction setting type requires the input to be a valid number between 0 and 1.'
|
||||
end
|
||||
|
||||
if input < 0 then
|
||||
input = 0
|
||||
end
|
||||
|
||||
if input > 1 then
|
||||
input = 1
|
||||
end
|
||||
|
||||
return true, input
|
||||
end,
|
||||
string = function (input)
|
||||
if input == nil then
|
||||
return true, ''
|
||||
end
|
||||
|
||||
local input_type = type(input)
|
||||
if input_type == 'string' then
|
||||
return true, input
|
||||
end
|
||||
|
||||
if input_type == 'number' or input_type == 'boolean' then
|
||||
return true, tostring(input)
|
||||
end
|
||||
|
||||
return false, 'string setting type requires the input to be either a valid string or something that can be converted to a string.'
|
||||
end,
|
||||
boolean = function (input)
|
||||
local input_type = type(input)
|
||||
|
||||
if input_type == 'boolean' then
|
||||
return true, input
|
||||
end
|
||||
|
||||
if input_type == 'string' then
|
||||
if input == '0' or input == '' or input == 'false' or input == 'no' then
|
||||
return true, false
|
||||
end
|
||||
if input == '1' or input == 'true' or input == 'yes' then
|
||||
return true, true
|
||||
end
|
||||
|
||||
return true, tonumber(input) ~= nil
|
||||
end
|
||||
|
||||
if input_type == 'number' then
|
||||
return true, input ~= 0
|
||||
end
|
||||
|
||||
return false, 'boolean setting type requires the input to be either a boolean, number or string that can be transformed to a boolean.'
|
||||
end,
|
||||
}
|
||||
|
||||
local settings = {}
|
||||
local memory = {}
|
||||
|
||||
Global.register(memory, function (tbl) memory = tbl end)
|
||||
|
||||
local Public = {}
|
||||
|
||||
Public.types = {fraction = 'fraction', string = 'string', boolean = 'boolean'}
|
||||
|
||||
---Register a specific setting with a sensitization setting type.
|
||||
---
|
||||
--- Available setting types:
|
||||
--- - fraction (number between 0 and 1) in either number or string form
|
||||
--- - string a string or anything that can be cast to a string
|
||||
--- - boolean, 1, 0, yes, no, true, false or an empty string for false
|
||||
---
|
||||
--- This function must be called in the control stage, i.e. not inside an event.
|
||||
---
|
||||
---@param name string
|
||||
---@param setting_type string
|
||||
---@param default mixed
|
||||
function Public.register(name, setting_type, default)
|
||||
if _LIFECYCLE ~= _STAGE.control then
|
||||
error(format('You can only register setting names in the control stage, i.e. not inside events. Tried setting "%s" with type "%s".', name, setting_type), 2)
|
||||
end
|
||||
|
||||
if settings[name] then
|
||||
error(format('Trying to register setting for "%s" while it has already been registered.', name), 2)
|
||||
end
|
||||
|
||||
local callback = settings_type[setting_type]
|
||||
if not callback then
|
||||
error(format('Trying to register setting for "%s" with type "%s" while this type does not exist.', name, setting_type), 2)
|
||||
end
|
||||
|
||||
local setting = {
|
||||
default = default,
|
||||
callback = callback,
|
||||
}
|
||||
|
||||
settings[name] = setting
|
||||
|
||||
return setting
|
||||
end
|
||||
|
||||
---Sets a setting to a specific value for a player.
|
||||
---
|
||||
---In order to get a setting value, it has to be registered via the "register" function.
|
||||
---
|
||||
---@param player_index number
|
||||
---@param name string
|
||||
---@param value mixed
|
||||
function Public.set(player_index, name, value)
|
||||
local setting = settings[name]
|
||||
if not setting then
|
||||
return error(format('Setting "%s" does not exist.', name), 2)
|
||||
end
|
||||
|
||||
local success, sanitized_value = setting.callback(value)
|
||||
|
||||
if not success then
|
||||
error(format('Setting "%s" failed: %s', name, sanitized_value), 2)
|
||||
end
|
||||
|
||||
local player_settings = memory[player_index]
|
||||
if not player_settings then
|
||||
player_settings = {}
|
||||
memory[player_index] = player_settings
|
||||
end
|
||||
|
||||
player_settings[name] = sanitized_value
|
||||
|
||||
return sanitized_value
|
||||
end
|
||||
|
||||
---Returns the value of a setting for this player.
|
||||
---
|
||||
---In order to set a setting value, it has to be registered via the "register" function.
|
||||
---
|
||||
---@param player_index number
|
||||
---@param name string
|
||||
function Public.get(player_index, name)
|
||||
local setting = settings[name]
|
||||
if not setting then
|
||||
return error(format('Setting "%s" does not exist.', name), 2)
|
||||
end
|
||||
|
||||
local player_settings = memory[player_index]
|
||||
if not player_settings then
|
||||
return setting.default
|
||||
end
|
||||
|
||||
local player_setting = player_settings[name]
|
||||
return player_setting ~= nil and player_setting or setting.default
|
||||
end
|
||||
|
||||
---Returns a table of all settings for a given player in a key => value set-up
|
||||
---@param player_index number
|
||||
function Public.all(player_index)
|
||||
local player_settings = memory[player_index] or {}
|
||||
local output = {}
|
||||
for name, data in pairs(settings) do
|
||||
output[name] = player_settings[name] or data.default
|
||||
end
|
||||
|
||||
return output
|
||||
end
|
||||
|
||||
return Public
|
||||
@@ -1,8 +0,0 @@
|
||||
local loaded = _G.package.loaded
|
||||
local raw_require = require
|
||||
|
||||
function require(path)
|
||||
return loaded[path] or error('Can only require files at runtime that have been required in the control stage.', 2)
|
||||
end
|
||||
|
||||
return raw_require
|
||||
@@ -1,119 +0,0 @@
|
||||
--- This module provides a classical mealy/moore state machine.
|
||||
-- Each machine in constructed by calling new()
|
||||
-- States and Transitions are lazily added to the machine as transition handlers and state tick handlers are registered.
|
||||
-- However the state machine must be fully defined after init is done. Dynamic machine changes are currently unsupported
|
||||
-- An example usage can be found here: map_gen\combined\tetris\control.lua
|
||||
|
||||
local Module = {}
|
||||
|
||||
local Debug = require 'utils.debug' --- @dep utils.debug
|
||||
|
||||
local in_state_callbacks = {}
|
||||
local transaction_callbacks = {}
|
||||
local max_stack_depth = 20
|
||||
local machine_count = 0
|
||||
local control_stage = _STAGE.control
|
||||
|
||||
--- Transitions the supplied machine into a given state and executes all transaction_callbacks
|
||||
-- @param self StateMachine
|
||||
-- @param new_state number/string The new state to transition to
|
||||
function Module.transition(self, new_state)
|
||||
Debug.print(string.format('Transitioning from state %d to state %d.', self.state, new_state))
|
||||
local old_state = self.state
|
||||
|
||||
local stack_depth = self.stack_depth
|
||||
self.stack_depth = stack_depth + 1
|
||||
if stack_depth > max_stack_depth then
|
||||
if _DEBUG then
|
||||
error('[WARNING] Stack overflow at:' .. debug.traceback())
|
||||
else
|
||||
log('[WARNING] Stack overflow at:' .. debug.traceback())
|
||||
end
|
||||
end
|
||||
|
||||
local exit_callbacks = transaction_callbacks[self.id][old_state]
|
||||
if exit_callbacks then
|
||||
local entry_callbacks = exit_callbacks[new_state]
|
||||
if entry_callbacks then
|
||||
for i = 1, #entry_callbacks do
|
||||
local callback = entry_callbacks[i]
|
||||
if callback then
|
||||
callback()
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
self.state = new_state
|
||||
end
|
||||
|
||||
--- Is this machine in this state?
|
||||
-- @param self StateMachine
|
||||
-- @param state number/string
|
||||
-- @return boolean
|
||||
function Module.in_state(self, state)
|
||||
return self.state == state
|
||||
end
|
||||
|
||||
--- Invoke a machine tick. Will execute all in_state_callbacks of the given machine
|
||||
-- @param self StateMachine the machine, whose handlers will be invoked
|
||||
function Module.machine_tick(self)
|
||||
local callbacks = in_state_callbacks[self.id][self.state]
|
||||
if callbacks then
|
||||
for i=1, #callbacks do
|
||||
local callback = callbacks[i]
|
||||
if callback then
|
||||
callback()
|
||||
end
|
||||
end
|
||||
end
|
||||
self.stack_depth = 0
|
||||
end
|
||||
|
||||
--- Register a handler that will be invoked by StateMachine.machine_tick
|
||||
-- You may register multiple handlers for the same transition
|
||||
-- NOTICE: This function will invoke an error if called after init. Dynamic machine changes are currently unsupported
|
||||
-- @param self StateMachine the machine
|
||||
-- @param state number/string The state, that the machine will be in, when callback is invoked
|
||||
-- @param callback function
|
||||
function Module.register_state_tick_callback(self, state, callback)
|
||||
if _LIFECYCLE ~= control_stage then
|
||||
error('Calling StateMachine.register_state_tick_callback after the control stage is unsupported due to desyncs.', 2)
|
||||
end
|
||||
in_state_callbacks[self.id][state] = in_state_callbacks[self.id][state] or {}
|
||||
table.insert(in_state_callbacks[self.id][state], callback)
|
||||
end
|
||||
|
||||
--- Register a handler that will be invoked by StateMachine.transition
|
||||
-- You may register multiple handlers for the same transition
|
||||
-- NOTICE: This function will invoke an error if called after init. Dynamic machine changes are currently unsupported
|
||||
-- @param self StateMachine the machine
|
||||
-- @param old number/string exiting state
|
||||
-- @param new number/string entering state
|
||||
-- @param callback function
|
||||
function Module.register_transition_callback(self, old, new, callback)
|
||||
if _LIFECYCLE ~= control_stage then
|
||||
error('Calling StateMachine.register_transition_callback after the control stage is unsupported due to desyncs.', 2)
|
||||
end
|
||||
transaction_callbacks[self.id][old] = transaction_callbacks[self.id][old] or {}
|
||||
transaction_callbacks[self.id][old][new] = transaction_callbacks[self.id][old][new] or {}
|
||||
table.insert(transaction_callbacks[self.id][old][new], callback)
|
||||
end
|
||||
|
||||
--- Constructs a new state machine
|
||||
-- @param init_state number/string The starting state of the machine
|
||||
-- @return StateMachine The constructed state machine object
|
||||
function Module.new(init_state)
|
||||
if _LIFECYCLE ~= control_stage then
|
||||
error('Calling StateMachine.new after the control stage is unsupported due to desyncs.', 2)
|
||||
end
|
||||
machine_count = machine_count + 1
|
||||
in_state_callbacks[machine_count] = {}
|
||||
transaction_callbacks[machine_count] = {}
|
||||
return {
|
||||
state = init_state,
|
||||
stack_depth = 0,
|
||||
id = machine_count,
|
||||
}
|
||||
end
|
||||
|
||||
return Module
|
||||
265
utils/table.lua
265
utils/table.lua
@@ -1,265 +0,0 @@
|
||||
--luacheck:ignore global table
|
||||
local random = math.random
|
||||
local floor = math.floor
|
||||
local remove = table.remove
|
||||
local tonumber = tonumber
|
||||
local pairs = pairs
|
||||
local table_size = table_size
|
||||
|
||||
--- Searches a table to remove a specific element without an index
|
||||
-- @param t <table> to search
|
||||
-- @param <any> table element to search for
|
||||
function table.remove_element(t, element)
|
||||
for k, v in pairs(t) do
|
||||
if v == element then
|
||||
remove(t, k)
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- Removes an item from an array in O(1) time.
|
||||
-- The catch is that fast_remove doesn't guarantee to maintain the order of items in the array.
|
||||
-- @param tbl <table> arrayed table
|
||||
-- @param index <number> Must be >= 0. The case where index > #tbl is handled.
|
||||
function table.fast_remove(tbl, index)
|
||||
local count = #tbl
|
||||
if index > count then
|
||||
return
|
||||
elseif index < count then
|
||||
tbl[index] = tbl[count]
|
||||
end
|
||||
|
||||
tbl[count] = nil
|
||||
end
|
||||
|
||||
--- Adds the contents of table t2 to table t1
|
||||
-- @param t1 <table> to insert into
|
||||
-- @param t2 <table> to insert from
|
||||
function table.add_all(t1, t2)
|
||||
for k, v in pairs(t2) do
|
||||
if tonumber(k) then
|
||||
t1[#t1 + 1] = v
|
||||
else
|
||||
t1[k] = v
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- Checks if a table contains an element
|
||||
-- @param t <table>
|
||||
-- @param e <any> table element
|
||||
-- @return <any> the index of the element or nil
|
||||
function table.index_of(t, e)
|
||||
for k, v in pairs(t) do
|
||||
if v == e then
|
||||
return k
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
--- Checks if the arrayed portion of a table contains an element
|
||||
-- @param t <table>
|
||||
-- @param e <any> table element
|
||||
-- @return <number|nil> the index of the element or nil
|
||||
function table.index_of_in_array(t, e)
|
||||
for i = 1, #t do
|
||||
if t[i] == e then
|
||||
return i
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
local index_of = table.index_of
|
||||
--- Checks if a table contains an element
|
||||
-- @param t <table>
|
||||
-- @param e <any> table element
|
||||
-- @return <boolean> indicating success
|
||||
function table.contains(t, e)
|
||||
return index_of(t, e) and true or false
|
||||
end
|
||||
|
||||
local index_of_in_array = table.index_of_in_array
|
||||
--- Checks if the arrayed portion of a table contains an element
|
||||
-- @param t <table>
|
||||
-- @param e <any> table element
|
||||
-- @return <boolean> indicating success
|
||||
function table.array_contains(t, e)
|
||||
return index_of_in_array(t, e) and true or false
|
||||
end
|
||||
|
||||
--- Adds an element into a specific index position while shuffling the rest down
|
||||
-- @param t <table> to add into
|
||||
-- @param index <number> the position in the table to add to
|
||||
-- @param element <any> to add to the table
|
||||
function table.set(t, index, element)
|
||||
local i = 1
|
||||
for k in pairs(t) do
|
||||
if i == index then
|
||||
t[k] = element
|
||||
return nil
|
||||
end
|
||||
i = i + 1
|
||||
end
|
||||
error('Index out of bounds', 2)
|
||||
end
|
||||
|
||||
--- Chooses a random entry from a table
|
||||
-- because this uses math.random, it cannot be used outside of events
|
||||
-- @param t <table>
|
||||
-- @param key <boolean> to indicate whether to return the key or value
|
||||
-- @return <any> a random element of table t
|
||||
function table.get_random_dictionary_entry(t, key)
|
||||
local target_index = random(1, table_size(t))
|
||||
local count = 1
|
||||
for k, v in pairs(t) do
|
||||
if target_index == count then
|
||||
if key then
|
||||
return k
|
||||
else
|
||||
return v
|
||||
end
|
||||
end
|
||||
count = count + 1
|
||||
end
|
||||
end
|
||||
|
||||
--- Chooses a random entry from a weighted table
|
||||
-- because this uses math.random, it cannot be used outside of events
|
||||
-- @param weighted_table <table> of tables with items and their weights
|
||||
-- @param item_index <number> of the index of items, defaults to 1
|
||||
-- @param weight_index <number> of the index of the weights, defaults to 2
|
||||
-- @return <any> table element
|
||||
function table.get_random_weighted(weighted_table, item_index, weight_index)
|
||||
local total_weight = 0
|
||||
item_index = item_index or 1
|
||||
weight_index = weight_index or 2
|
||||
|
||||
for _, w in pairs(weighted_table) do
|
||||
total_weight = total_weight + w[weight_index]
|
||||
end
|
||||
|
||||
local index = random() * total_weight
|
||||
local weight_sum = 0
|
||||
for _, w in pairs(weighted_table) do
|
||||
weight_sum = weight_sum + w[weight_index]
|
||||
if weight_sum >= index then
|
||||
return w[item_index]
|
||||
end
|
||||
end
|
||||
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
|
||||
-- @param t <table> to shuffle
|
||||
-- @param rng <function> to provide random numbers
|
||||
function table.shuffle_table(t, rng)
|
||||
local rand = rng or math.random
|
||||
local iterations = #t
|
||||
if iterations == 0 then
|
||||
error('Not a sequential table')
|
||||
return
|
||||
end
|
||||
local j
|
||||
|
||||
for i = iterations, 2, -1 do
|
||||
j = rand(i)
|
||||
t[i], t[j] = t[j], t[i]
|
||||
end
|
||||
end
|
||||
|
||||
--- Clears all existing entries in a table
|
||||
-- @param t <table> to clear
|
||||
-- @param array <boolean> to indicate whether the table is an array or not
|
||||
function table.clear_table(t, array)
|
||||
if array then
|
||||
for i = 1, #t do
|
||||
t[i] = nil
|
||||
end
|
||||
else
|
||||
for i in pairs(t) do
|
||||
t[i] = nil
|
||||
end
|
||||
end
|
||||
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)
|
||||
--For some reason bit32.bnot doesn't return negative numbers so I'm using ~x = -1 - x instead.
|
||||
|
||||
local lower = 1
|
||||
local upper = #t
|
||||
|
||||
if upper == 0 then
|
||||
return -2 -- ~1
|
||||
end
|
||||
|
||||
repeat
|
||||
local mid = floor((lower + upper) * 0.5)
|
||||
local value = t[mid]
|
||||
if value == target then
|
||||
return mid
|
||||
elseif value < target then
|
||||
lower = mid + 1
|
||||
else
|
||||
upper = mid - 1
|
||||
end
|
||||
until lower > upper
|
||||
|
||||
return -1 - lower -- ~lower
|
||||
end
|
||||
|
||||
-- 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.
|
||||
-- @param table <table> the table to serialize
|
||||
-- @param options <table> 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 <string> the prettied table
|
||||
table.inspect = require 'utils.inspect' --- @dep utils.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.
|
||||
-- @param object <table> the object to copy
|
||||
-- @return <table> the copied object
|
||||
table.deep_copy = table.deepcopy
|
||||
|
||||
--- 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}
|
||||
-- @param tables <table> takes a table of tables to merge
|
||||
-- @return <table> a merged table
|
||||
table.merge = util.merge
|
||||
|
||||
--- Determines if two tables are structurally equal.
|
||||
-- Notice: tables that are LuaObjects or contain LuaObjects won't be compared correctly, use == operator for LuaObjects
|
||||
-- @param tbl1 <table>
|
||||
-- @param tbl2 <table>
|
||||
-- @return <boolean>
|
||||
table.equals = table.compare
|
||||
|
||||
return table
|
||||
@@ -1,152 +0,0 @@
|
||||
--- source https://github.com/daurnimator/luatz/blob/master/luatz/timetable.lua
|
||||
-- edited down to just what is needed.
|
||||
|
||||
local Public = {}
|
||||
|
||||
local floor = math.floor
|
||||
local strformat = string.format
|
||||
|
||||
local function borrow(tens, units, base)
|
||||
local frac = tens % 1
|
||||
units = units + frac * base
|
||||
tens = tens - frac
|
||||
return tens, units
|
||||
end
|
||||
|
||||
local function carry(tens, units, base)
|
||||
if units >= base then
|
||||
tens = tens + floor(units / base)
|
||||
units = units % base
|
||||
elseif units < 0 then
|
||||
tens = tens + floor(units / base)
|
||||
units = (base + units) % base
|
||||
end
|
||||
return tens, units
|
||||
end
|
||||
|
||||
local function is_leap(y)
|
||||
if (y % 4) ~= 0 then
|
||||
return false
|
||||
elseif (y % 100) ~= 0 then
|
||||
return true
|
||||
else
|
||||
return (y % 400) == 0
|
||||
end
|
||||
end
|
||||
|
||||
local mon_lengths = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}
|
||||
|
||||
-- Number of days in year until start of month; not corrected for leap years
|
||||
local months_to_days_cumulative = {0}
|
||||
for i = 2, 12 do
|
||||
months_to_days_cumulative[i] = months_to_days_cumulative[i - 1] + mon_lengths[i - 1]
|
||||
end
|
||||
|
||||
local function month_length(m, y)
|
||||
if m == 2 then
|
||||
return is_leap(y) and 29 or 28
|
||||
else
|
||||
return mon_lengths[m]
|
||||
end
|
||||
end
|
||||
|
||||
local function day_of_year(day, month, year)
|
||||
local y_day = months_to_days_cumulative[month]
|
||||
if month > 2 and is_leap(year) then
|
||||
y_day = y_day + 1
|
||||
end
|
||||
return y_day + day
|
||||
end
|
||||
|
||||
local function leap_years_since(year)
|
||||
return floor(year / 4) - floor(year / 100) + floor(year / 400)
|
||||
end
|
||||
|
||||
local leap_years_since_1970 = leap_years_since(1970)
|
||||
|
||||
local function normalise(year, month, day, hour, min, sec)
|
||||
-- `month` and `day` start from 1, need -1 and +1 so it works modulo
|
||||
month, day = month - 1, day - 1
|
||||
|
||||
-- Convert everything (except seconds) to an integer
|
||||
-- by propagating fractional components down.
|
||||
year, month = borrow(year, month, 12)
|
||||
-- Carry from month to year first, so we get month length correct in next line around leap years
|
||||
year, month = carry(year, month, 12)
|
||||
month, day = borrow(month, day, month_length(floor(month + 1), year))
|
||||
day, hour = borrow(day, hour, 24)
|
||||
hour, min = borrow(hour, min, 60)
|
||||
min, sec = borrow(min, sec, 60)
|
||||
|
||||
-- Propagate out of range values up
|
||||
-- e.g. if `min` is 70, `hour` increments by 1 and `min` becomes 10
|
||||
-- This has to happen for all columns after borrowing, as lower radix's may be pushed out of range
|
||||
min, sec = carry(min, sec, 60) -- TODO: consider leap seconds?
|
||||
hour, min = carry(hour, min, 60)
|
||||
day, hour = carry(day, hour, 24)
|
||||
-- Ensure `day` is not underflowed
|
||||
-- Add a whole year of days at a time, this is later resolved by adding months
|
||||
-- TODO[OPTIMIZE]: This could be slow if `day` is far out of range
|
||||
while day < 0 do
|
||||
month = month - 1
|
||||
if month < 0 then
|
||||
year = year - 1
|
||||
month = 11
|
||||
end
|
||||
day = day + month_length(month + 1, year)
|
||||
end
|
||||
year, month = carry(year, month, 12)
|
||||
|
||||
-- TODO[OPTIMIZE]: This could potentially be slow if `day` is very large
|
||||
while true do
|
||||
local i = month_length(month + 1, year)
|
||||
if day < i then
|
||||
break
|
||||
end
|
||||
day = day - i
|
||||
month = month + 1
|
||||
if month >= 12 then
|
||||
month = 0
|
||||
year = year + 1
|
||||
end
|
||||
end
|
||||
|
||||
-- Now we can place `day` and `month` back in their normal ranges
|
||||
-- e.g. month as 1-12 instead of 0-11
|
||||
month, day = month + 1, day + 1
|
||||
|
||||
return {year = year, month = month, day = day, hour = hour, min = min, sec = sec}
|
||||
end
|
||||
|
||||
--- Converts unix epoch timestamp into table {year: number, month: number, day: number, hour: number, min: number, sec: number}
|
||||
-- @param seconds<number> unix epoch timestamp
|
||||
-- @return {year: number, month: number, day: number, hour: number, min: number, sec: number}
|
||||
function Public.to_timetable(seconds)
|
||||
return normalise(1970, 1, 1, 0, 0, seconds)
|
||||
end
|
||||
|
||||
--- Converts timetable into unix epoch timestamp
|
||||
-- @param timetable<table> {year: number, month: number, day: number, hour: number, min: number, sec: number}
|
||||
-- @return number
|
||||
function Public.from_timetable(timetable)
|
||||
local tt = normalise(timetable.year, timetable.month, timetable.day, timetable.hour, timetable.min, timetable.sec)
|
||||
|
||||
local year, month, day, hour, min, sec = tt.year, tt.month, tt.day, tt.hour, tt.min, tt.sec
|
||||
|
||||
local days_since_epoch =
|
||||
day_of_year(day, month, year) + 365 * (year - 1970) + -- Each leap year adds one day
|
||||
(leap_years_since(year - 1) - leap_years_since_1970) -
|
||||
1
|
||||
|
||||
return days_since_epoch * (60 * 60 * 24) + hour * (60 * 60) + min * 60 + sec
|
||||
end
|
||||
|
||||
--- Converts unix epoch timestamp into human readable string.
|
||||
-- @param seconds<type> unix epoch timestamp
|
||||
-- @return string
|
||||
function Public.to_string(seconds)
|
||||
local tt = normalise(1970, 1, 1, 0, 0, seconds)
|
||||
return strformat('%04u-%02u-%02u %02u:%02u:%02d', tt.year, tt.month, tt.day, tt.hour, tt.min, tt.sec)
|
||||
end
|
||||
|
||||
return Public
|
||||
Reference in New Issue
Block a user