mirror of
https://github.com/PHIDIAS0303/ExpCluster.git
synced 2025-12-30 04:21:41 +09:00
Refactor of commands
This commit is contained in:
179
utils/alien_evolution_progress.lua
Normal file
179
utils/alien_evolution_progress.lua
Normal file
@@ -0,0 +1,179 @@
|
||||
--[[-- 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'
|
||||
local Debug = require 'utils.debug'
|
||||
local table = require '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 spawners. If dividing the total spawners by the total aliens causes a fraction, the
|
||||
---fraction will decide a chance to spawn. 1 alien for 2 spawners 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
|
||||
326
utils/command.lua
Normal file
326
utils/command.lua
Normal file
@@ -0,0 +1,326 @@
|
||||
local Event = require 'utils.event'
|
||||
local Game = require 'utils.game'
|
||||
local Utils = require 'utils.core'
|
||||
local Timestamp = require 'utils.timestamp'
|
||||
local Rank = require 'features.rank_system'
|
||||
local Donator = require 'features.donator'
|
||||
local Server = require 'features.server'
|
||||
local Ranks = require '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 randomizer: 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 determins 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
|
||||
246
utils/core.lua
Normal file
246
utils/core.lua
Normal file
@@ -0,0 +1,246 @@
|
||||
-- This file contains core utilities used by the redmew scenario.
|
||||
|
||||
-- Dependencies
|
||||
local Game = require 'utils.game'
|
||||
local Color = require 'resources.color_presets'
|
||||
local Server = require 'features.server'
|
||||
|
||||
-- localized functions
|
||||
local random = math.random
|
||||
local sqrt = math.sqrt
|
||||
local floor = math.floor
|
||||
local format = string.format
|
||||
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 vars
|
||||
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
|
||||
|
||||
--- Takes msg and prints it to all players. Also prints to the log and discord
|
||||
-- @param msg <string> The message to print
|
||||
-- @param warning_prefix <string> The name of the module/warning
|
||||
function Module.action_warning(warning_prefix, msg)
|
||||
game.print(prefix .. msg, Color.yellow)
|
||||
msg = format('%s %s', warning_prefix, msg)
|
||||
log(msg)
|
||||
Server.to_discord_bold(msg)
|
||||
end
|
||||
|
||||
--- Takes msg and prints it to all players except provided player. Also prints to the log and discord
|
||||
-- @param msg <string> The message to print
|
||||
-- @param warning_prefix <string> The name of the module/warning
|
||||
-- @param player <LuaPlayer> the player not to send the message to
|
||||
function Module.silent_action_warning(warning_prefix, msg, player)
|
||||
Module.print_except(prefix .. msg, player, Color.yellow)
|
||||
msg = format('%s %s', warning_prefix, msg)
|
||||
log(msg)
|
||||
Server.to_discord_bold(msg)
|
||||
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
|
||||
@@ -1,13 +0,0 @@
|
||||
-- Info on the data lifecycle and how we use it: https://github.com/Refactorio/RedMew/wiki/The-data-lifecycle
|
||||
-- Non-applicable stages are commented out.
|
||||
_STAGE = {
|
||||
--settings = 1,
|
||||
--data = 2,
|
||||
--migration = 3,
|
||||
control = 4,
|
||||
init = 5,
|
||||
load = 6,
|
||||
--config_change = 7,
|
||||
runtime = 8
|
||||
}
|
||||
_LIFECYCLE = _STAGE.control
|
||||
32
utils/dump_env.lua
Normal file
32
utils/dump_env.lua
Normal file
@@ -0,0 +1,32 @@
|
||||
-- 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'
|
||||
local Event = require '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)
|
||||
120
utils/game.lua
Normal file
120
utils/game.lua
Normal file
@@ -0,0 +1,120 @@
|
||||
local Global = require 'utils.global'
|
||||
local Color = require 'resources.color_presets'
|
||||
local pairs = pairs
|
||||
|
||||
local Game = {}
|
||||
|
||||
local bad_name_players = {}
|
||||
Global.register(
|
||||
bad_name_players,
|
||||
function(tbl)
|
||||
bad_name_players = tbl
|
||||
end
|
||||
)
|
||||
|
||||
--[[
|
||||
Due to a bug in the Factorio api the following expression isn't guaranteed to be true.
|
||||
game.players[player.index] == player
|
||||
get_player_by_index(index) will always return the correct player.
|
||||
When looking up players by name or iterating through all players use game.players instead.
|
||||
]]
|
||||
function Game.get_player_by_index(index)
|
||||
local p = game.players[index]
|
||||
|
||||
if not p then
|
||||
return nil
|
||||
end
|
||||
if p.index == index then
|
||||
return p
|
||||
end
|
||||
|
||||
p = bad_name_players[index]
|
||||
if p then
|
||||
if p.valid then
|
||||
return p
|
||||
else
|
||||
return nil
|
||||
end
|
||||
end
|
||||
|
||||
for k, v in pairs(game.players) do
|
||||
if k == index then
|
||||
bad_name_players[index] = v
|
||||
return v
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- Returns a valid LuaPlayer if given a number, string, or LuaPlayer. Returns nil otherwise.
|
||||
-- obj <number|string|LuaPlayer>
|
||||
function Game.get_player_from_any(obj)
|
||||
local o_type = type(obj)
|
||||
local p
|
||||
if type == 'number' then
|
||||
p = Game.get_player_by_index(obj)
|
||||
elseif o_type == 'string' then
|
||||
p = game.players[obj]
|
||||
elseif o_type == 'table' and obj.valid and obj.is_player() then
|
||||
return obj
|
||||
end
|
||||
|
||||
if p and p.valid then
|
||||
return p
|
||||
end
|
||||
end
|
||||
|
||||
--- Prints to player or console.
|
||||
-- @param str <string|table> table if locale is used
|
||||
-- @param color <table> defaults to white
|
||||
function Game.player_print(str, color)
|
||||
color = color or Color.white
|
||||
if game.player then
|
||||
game.player.print(str, color)
|
||||
else
|
||||
print(str)
|
||||
end
|
||||
end
|
||||
|
||||
--[[
|
||||
@param Position String to display at
|
||||
@param text String to display
|
||||
@param color table in {r = 0~1, g = 0~1, b = 0~1}, defaults to white.
|
||||
@param surface LuaSurface
|
||||
|
||||
@return the created entity
|
||||
]]
|
||||
function Game.print_floating_text(surface, position, text, color)
|
||||
color = color or Color.white
|
||||
|
||||
return surface.create_entity {
|
||||
name = 'tutorial-flying-text',
|
||||
color = color,
|
||||
text = text,
|
||||
position = position
|
||||
}
|
||||
end
|
||||
|
||||
--[[
|
||||
Creates a floating text entity at the player location with the specified color in {r, g, b} format.
|
||||
Example: "+10 iron" or "-10 coins"
|
||||
|
||||
@param text String to display
|
||||
@param color table in {r = 0~1, g = 0~1, b = 0~1}, defaults to white.
|
||||
|
||||
@return the created entity
|
||||
]]
|
||||
function Game.print_player_floating_text_position(player_index, text, color, x_offset, y_offset)
|
||||
local player = Game.get_player_by_index(player_index)
|
||||
if not player or not player.valid then
|
||||
return
|
||||
end
|
||||
|
||||
local position = player.position
|
||||
return Game.print_floating_text(player.surface, {x = position.x + x_offset, y = position.y + y_offset}, text, color)
|
||||
end
|
||||
|
||||
function Game.print_player_floating_text(player_index, text, color)
|
||||
Game.print_player_floating_text_position(player_index, text, color, 0, -1.5)
|
||||
end
|
||||
|
||||
return Game
|
||||
287
utils/gui.lua
Normal file
287
utils/gui.lua
Normal file
@@ -0,0 +1,287 @@
|
||||
local Token = require 'utils.token'
|
||||
local Event = require 'utils.event'
|
||||
local Game = require 'utils.game'
|
||||
local Global = require 'utils.global'
|
||||
|
||||
local Gui = {}
|
||||
|
||||
local data = {}
|
||||
|
||||
Global.register(
|
||||
data,
|
||||
function(tbl)
|
||||
data = tbl
|
||||
end
|
||||
)
|
||||
|
||||
local top_elements = {}
|
||||
local on_visible_handlers = {}
|
||||
local on_pre_hidden_handlers = {}
|
||||
|
||||
function Gui.uid_name()
|
||||
return tostring(Token.uid())
|
||||
end
|
||||
|
||||
-- Associates data with the LuaGuiElement. If data is nil then removes the data
|
||||
function Gui.set_data(element, value)
|
||||
data[element.player_index * 0x100000000 + element.index] = value
|
||||
end
|
||||
|
||||
-- Gets the Associated data with this LuaGuiElement if any.
|
||||
function Gui.get_data(element)
|
||||
return data[element.player_index * 0x100000000 + element.index]
|
||||
end
|
||||
|
||||
-- Removes data associated with LuaGuiElement and its children recursively.
|
||||
function Gui.remove_data_recursively(element)
|
||||
Gui.set_data(element, nil)
|
||||
|
||||
local children = element.children
|
||||
|
||||
if not children then
|
||||
return
|
||||
end
|
||||
|
||||
for _, child in ipairs(children) do
|
||||
if child.valid then
|
||||
Gui.remove_data_recursively(child)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function Gui.remove_children_data(element)
|
||||
local children = element.children
|
||||
|
||||
if not children then
|
||||
return
|
||||
end
|
||||
|
||||
for _, child in ipairs(children) do
|
||||
if child.valid then
|
||||
Gui.set_data(child, nil)
|
||||
Gui.remove_children_data(child)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function Gui.destroy(element)
|
||||
Gui.remove_data_recursively(element)
|
||||
element.destroy()
|
||||
end
|
||||
|
||||
function Gui.clear(element)
|
||||
Gui.remove_children_data(element)
|
||||
element.clear()
|
||||
end
|
||||
|
||||
local function handler_factory(event_id)
|
||||
local handlers
|
||||
|
||||
local function on_event(event)
|
||||
local element = event.element
|
||||
if not element or not element.valid then
|
||||
return
|
||||
end
|
||||
|
||||
local handler = handlers[element.name]
|
||||
if not handler then
|
||||
return
|
||||
end
|
||||
|
||||
local player = Game.get_player_by_index(event.player_index)
|
||||
if not player or not player.valid then
|
||||
return
|
||||
end
|
||||
event.player = player
|
||||
|
||||
handler(event)
|
||||
end
|
||||
|
||||
return function(element_name, handler)
|
||||
if not handlers then
|
||||
handlers = {}
|
||||
Event.add(event_id, on_event)
|
||||
end
|
||||
|
||||
handlers[element_name] = handler
|
||||
end
|
||||
end
|
||||
|
||||
local function custom_handler_factory(handlers)
|
||||
return function(element_name, handler)
|
||||
handlers[element_name] = handler
|
||||
end
|
||||
end
|
||||
|
||||
local function custom_raise(handlers, element, player)
|
||||
local handler = handlers[element.name]
|
||||
if not handler then
|
||||
return
|
||||
end
|
||||
|
||||
handler({element = element, player = player})
|
||||
end
|
||||
|
||||
-- Register a handler for the on_gui_checked_state_changed event for LuaGuiElements with element_name.
|
||||
-- Can only have one handler per element name.
|
||||
-- Guarantees that the element and the player are valid when calling the handler.
|
||||
-- Adds a player field to the event table.
|
||||
Gui.on_checked_state_changed = handler_factory(defines.events.on_gui_checked_state_changed)
|
||||
|
||||
-- Register a handler for the on_gui_click event for LuaGuiElements with element_name.
|
||||
-- Can only have one handler per element name.
|
||||
-- Guarantees that the element and the player are valid when calling the handler.
|
||||
-- Adds a player field to the event table.
|
||||
Gui.on_click = handler_factory(defines.events.on_gui_click)
|
||||
|
||||
-- Register a handler for the on_gui_closed event for a custom LuaGuiElements with element_name.
|
||||
-- Can only have one handler per element name.
|
||||
-- Guarantees that the element and the player are valid when calling the handler.
|
||||
-- Adds a player field to the event table.
|
||||
Gui.on_custom_close = handler_factory(defines.events.on_gui_closed)
|
||||
|
||||
-- Register a handler for the on_gui_elem_changed event for LuaGuiElements with element_name.
|
||||
-- Can only have one handler per element name.
|
||||
-- Guarantees that the element and the player are valid when calling the handler.
|
||||
-- Adds a player field to the event table.
|
||||
Gui.on_elem_changed = handler_factory(defines.events.on_gui_elem_changed)
|
||||
|
||||
-- Register a handler for the on_gui_selection_state_changed event for LuaGuiElements with element_name.
|
||||
-- Can only have one handler per element name.
|
||||
-- Guarantees that the element and the player are valid when calling the handler.
|
||||
-- Adds a player field to the event table.
|
||||
Gui.on_selection_state_changed = handler_factory(defines.events.on_gui_selection_state_changed)
|
||||
|
||||
-- Register a handler for the on_gui_text_changed event for LuaGuiElements with element_name.
|
||||
-- Can only have one handler per element name.
|
||||
-- Guarantees that the element and the player are valid when calling the handler.
|
||||
-- Adds a player field to the event table.
|
||||
Gui.on_text_changed = handler_factory(defines.events.on_gui_text_changed)
|
||||
|
||||
-- Register a handler for the on_gui_value_changed event for LuaGuiElements with element_name.
|
||||
-- Can only have one handler per element name.
|
||||
-- Guarantees that the element and the player are valid when calling the handler.
|
||||
-- Adds a player field to the event table.
|
||||
Gui.on_value_changed = handler_factory(defines.events.on_gui_value_changed)
|
||||
|
||||
-- Register a handler for when the player shows the top LuaGuiElements with element_name.
|
||||
-- Assuming the element_name has been added with Gui.allow_player_to_toggle_top_element_visibility.
|
||||
-- Can only have one handler per element name.
|
||||
-- Guarantees that the element and the player are valid when calling the handler.
|
||||
-- Adds a player field to the event table.
|
||||
Gui.on_player_show_top = custom_handler_factory(on_visible_handlers)
|
||||
|
||||
-- Register a handler for when the player hides the top LuaGuiElements with element_name.
|
||||
-- Assuming the element_name has been added with Gui.allow_player_to_toggle_top_element_visibility.
|
||||
-- Can only have one handler per element name.
|
||||
-- Guarantees that the element and the player are valid when calling the handler.
|
||||
-- Adds a player field to the event table.
|
||||
Gui.on_pre_player_hide_top = custom_handler_factory(on_pre_hidden_handlers)
|
||||
|
||||
--- Allows the player to show / hide this element.
|
||||
-- The element must be part in gui.top.
|
||||
-- This function must be called in the control stage, i.e not inside an event.
|
||||
-- @param element_name<string> This name must be globally unique.
|
||||
function Gui.allow_player_to_toggle_top_element_visibility(element_name)
|
||||
if _LIFECYCLE ~= _STAGE.control then
|
||||
error('can only be called during the control stage', 2)
|
||||
end
|
||||
top_elements[#top_elements + 1] = element_name
|
||||
end
|
||||
|
||||
local toggle_button_name = Gui.uid_name()
|
||||
|
||||
Event.add(
|
||||
defines.events.on_player_created,
|
||||
function(event)
|
||||
local player = Game.get_player_by_index(event.player_index)
|
||||
|
||||
if not player or not player.valid then
|
||||
return
|
||||
end
|
||||
|
||||
local b =
|
||||
player.gui.top.add {
|
||||
type = 'button',
|
||||
name = toggle_button_name,
|
||||
caption = '<',
|
||||
tooltip = 'Shows / hides the Redmew Gui buttons.'
|
||||
}
|
||||
local style = b.style
|
||||
style.width = 18
|
||||
style.height = 38
|
||||
style.left_padding = 0
|
||||
style.top_padding = 0
|
||||
style.right_padding = 0
|
||||
style.bottom_padding = 0
|
||||
style.font = 'default-small-bold'
|
||||
end
|
||||
)
|
||||
|
||||
Gui.on_click(
|
||||
toggle_button_name,
|
||||
function(event)
|
||||
local button = event.element
|
||||
local player = event.player
|
||||
local top = player.gui.top
|
||||
|
||||
if button.caption == '<' then
|
||||
for i = 1, #top_elements do
|
||||
local name = top_elements[i]
|
||||
local ele = top[name]
|
||||
if ele and ele.valid then
|
||||
local style = ele.style
|
||||
|
||||
-- if visible is not set it has the value of nil.
|
||||
-- Hence nil is treated as is visible.
|
||||
local v = style.visible
|
||||
if v or v == nil then
|
||||
custom_raise(on_pre_hidden_handlers, ele, player)
|
||||
style.visible = false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
button.caption = '>'
|
||||
button.style.height = 24
|
||||
else
|
||||
for i = 1, #top_elements do
|
||||
local name = top_elements[i]
|
||||
local ele = top[name]
|
||||
if ele and ele.valid then
|
||||
local style = ele.style
|
||||
|
||||
if not style.visible then
|
||||
style.visible = true
|
||||
custom_raise(on_visible_handlers, ele, player)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
button.caption = '<'
|
||||
button.style.height = 38
|
||||
end
|
||||
end
|
||||
)
|
||||
|
||||
if _DEBUG then
|
||||
local concat = table.concat
|
||||
|
||||
local names = {}
|
||||
Gui.names = names
|
||||
|
||||
function Gui.uid_name()
|
||||
local info = debug.getinfo(2, 'Sl')
|
||||
local filepath = info.source:match('^.+/currently%-playing/(.+)$'):sub(1, -5)
|
||||
local line = info.currentline
|
||||
|
||||
local token = tostring(Token.uid())
|
||||
|
||||
local name = concat {token, ' - ', filepath, ':line:', line}
|
||||
names[token] = name
|
||||
|
||||
return token
|
||||
end
|
||||
end
|
||||
|
||||
return Gui
|
||||
342
utils/inspect.lua
Normal file
342
utils/inspect.lua
Normal file
@@ -0,0 +1,342 @@
|
||||
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
|
||||
|
||||
54
utils/math.lua
Normal file
54
utils/math.lua
Normal file
@@ -0,0 +1,54 @@
|
||||
--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 - cordinates of a point on a line
|
||||
-- @param x2, y2 numbers - cordinates 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, y numbers - 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
|
||||
154
utils/player_rewards.lua
Normal file
154
utils/player_rewards.lua
Normal file
@@ -0,0 +1,154 @@
|
||||
local Global = require 'utils.global'
|
||||
local Game = require 'utils.game'
|
||||
local PlayerStats = require 'features.player_stats'
|
||||
local Command = require 'utils.command'
|
||||
local Ranks = require '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
|
||||
13
utils/print_override.lua
Normal file
13
utils/print_override.lua
Normal file
@@ -0,0 +1,13 @@
|
||||
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
|
||||
74
utils/priority_queue.lua
Normal file
74
utils/priority_queue.lua
Normal file
@@ -0,0 +1,74 @@
|
||||
local PriorityQueue = {}
|
||||
|
||||
function PriorityQueue.new()
|
||||
return {}
|
||||
end
|
||||
|
||||
local function default_comp(a, b)
|
||||
return a < b
|
||||
end
|
||||
|
||||
local function HeapifyFromEndToStart(queue, comp)
|
||||
comp = comp or default_comp
|
||||
local pos = #queue
|
||||
while pos > 1 do
|
||||
local parent = bit32.rshift(pos, 1) -- integer division by 2
|
||||
if comp(queue[pos], queue[parent]) then
|
||||
queue[pos], queue[parent] = queue[parent], queue[pos]
|
||||
pos = parent
|
||||
else
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function HeapifyFromStartToEnd(queue, comp)
|
||||
comp = comp or default_comp
|
||||
local parent = 1
|
||||
local smallest = 1
|
||||
while true do
|
||||
local child = parent * 2
|
||||
if child > #queue then
|
||||
break
|
||||
end
|
||||
if comp(queue[child], queue[parent]) then
|
||||
smallest = child
|
||||
end
|
||||
child = child + 1
|
||||
if child <= #queue and comp(queue[child], queue[smallest]) then
|
||||
smallest = child
|
||||
end
|
||||
|
||||
if parent ~= smallest then
|
||||
queue[parent], queue[smallest] = queue[smallest], queue[parent]
|
||||
parent = smallest
|
||||
else
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function PriorityQueue.size(queue)
|
||||
return #queue
|
||||
end
|
||||
|
||||
function PriorityQueue.push(queue, element, comp)
|
||||
table.insert(queue, element)
|
||||
HeapifyFromEndToStart(queue, comp)
|
||||
end
|
||||
|
||||
function PriorityQueue.pop(queue, comp)
|
||||
local element = queue[1]
|
||||
|
||||
queue[1] = queue[#queue]
|
||||
queue[#queue] = nil
|
||||
HeapifyFromStartToEnd(queue, comp)
|
||||
|
||||
return element
|
||||
end
|
||||
|
||||
function PriorityQueue.peek(queue)
|
||||
return queue[1]
|
||||
end
|
||||
|
||||
return PriorityQueue
|
||||
34
utils/queue.lua
Normal file
34
utils/queue.lua
Normal file
@@ -0,0 +1,34 @@
|
||||
local Queue = {}
|
||||
|
||||
function Queue.new()
|
||||
local queue = {_head = 0, _tail = 0}
|
||||
return queue
|
||||
end
|
||||
|
||||
function Queue.size(queue)
|
||||
return queue._tail - queue._head
|
||||
end
|
||||
|
||||
function Queue.push(queue, element)
|
||||
local index = queue._head
|
||||
queue[index] = element
|
||||
queue._head = index - 1
|
||||
end
|
||||
|
||||
function Queue.peek(queue)
|
||||
return queue[queue._tail]
|
||||
end
|
||||
|
||||
function Queue.pop(queue)
|
||||
local index = queue._tail
|
||||
|
||||
local element = queue[index]
|
||||
queue[index] = nil
|
||||
|
||||
if element then
|
||||
queue._tail = index - 1
|
||||
end
|
||||
return element
|
||||
end
|
||||
|
||||
return Queue
|
||||
58
utils/recipe_locker.lua
Normal file
58
utils/recipe_locker.lua
Normal file
@@ -0,0 +1,58 @@
|
||||
-- A module to prevent recipes from being unlocked by research. Accessed via the public functions.
|
||||
local Event = require 'utils.event'
|
||||
local Global = require '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
|
||||
178
utils/redmew_settings.lua
Normal file
178
utils/redmew_settings.lua
Normal file
@@ -0,0 +1,178 @@
|
||||
local Global = require '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 callables 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 setup
|
||||
---@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
|
||||
119
utils/state_machine.lua
Normal file
119
utils/state_machine.lua
Normal file
@@ -0,0 +1,119 @@
|
||||
--- 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'
|
||||
|
||||
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 state number/string exiting state
|
||||
-- @param state 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
Normal file
265
utils/table.lua
Normal file
@@ -0,0 +1,265 @@
|
||||
--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
|
||||
-- @returns <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
|
||||
-- @returns <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
|
||||
-- @returns <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
|
||||
-- @returns <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 weight_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
|
||||
-- @see features.chat_triggers::hodor
|
||||
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
|
||||
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'
|
||||
|
||||
--- 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
|
||||
115
utils/task.lua
Normal file
115
utils/task.lua
Normal file
@@ -0,0 +1,115 @@
|
||||
-- Threading simulation module
|
||||
-- Task.sleep()
|
||||
-- @author Valansch and Grilledham
|
||||
-- github: https://github.com/Refactorio/RedMew
|
||||
-- ======================================================= --
|
||||
|
||||
local Queue = require 'utils.queue'
|
||||
local PriorityQueue = require 'utils.priority_queue'
|
||||
local Event = require 'utils.event'
|
||||
local Token = require 'utils.token'
|
||||
|
||||
local Task = {}
|
||||
|
||||
global.callbacks = global.callbacks or PriorityQueue.new()
|
||||
global.next_async_callback_time = -1
|
||||
global.task_queue = global.task_queue or Queue.new()
|
||||
global.total_task_weight = 0
|
||||
global.task_queue_speed = 1
|
||||
|
||||
local function comp(a, b)
|
||||
return a.time < b.time
|
||||
end
|
||||
|
||||
global.tpt = global.task_queue_speed
|
||||
local function get_task_per_tick()
|
||||
if game.tick % 300 == 0 then
|
||||
local size = global.total_task_weight
|
||||
global.tpt = math.floor(math.log10(size + 1)) * global.task_queue_speed
|
||||
if global.tpt < 1 then
|
||||
global.tpt = 1
|
||||
end
|
||||
end
|
||||
return global.tpt
|
||||
end
|
||||
|
||||
local function on_tick()
|
||||
local queue = global.task_queue
|
||||
for i = 1, get_task_per_tick() do
|
||||
local task = Queue.peek(queue)
|
||||
if task ~= nil then
|
||||
-- result is error if not success else result is a boolean for if the task should stay in the queue.
|
||||
local success, result = pcall(Token.get(task.func_token), task.params)
|
||||
if not success then
|
||||
if _DEBUG then
|
||||
error(result)
|
||||
else
|
||||
log(result)
|
||||
end
|
||||
Queue.pop(queue)
|
||||
global.total_task_weight = global.total_task_weight - task.weight
|
||||
elseif not result then
|
||||
Queue.pop(queue)
|
||||
global.total_task_weight = global.total_task_weight - task.weight
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local callbacks = global.callbacks
|
||||
local callback = PriorityQueue.peek(callbacks)
|
||||
while callback ~= nil and game.tick >= callback.time do
|
||||
local success, result = pcall(Token.get(callback.func_token), callback.params)
|
||||
if not success then
|
||||
if _DEBUG then
|
||||
error(result)
|
||||
else
|
||||
log(result)
|
||||
end
|
||||
end
|
||||
PriorityQueue.pop(callbacks, comp)
|
||||
callback = PriorityQueue.peek(callbacks)
|
||||
end
|
||||
end
|
||||
|
||||
--- Allows you to set a timer (in ticks) after which the tokened function will be run with params given as an argument
|
||||
-- Cannot be called before init
|
||||
-- @param ticks <number>
|
||||
-- @param func_token <number> a token for a function store via the token system
|
||||
-- @param params <any> the argument to send to the tokened function
|
||||
function Task.set_timeout_in_ticks(ticks, func_token, params)
|
||||
if not game then
|
||||
error('cannot call when game is not available', 2)
|
||||
end
|
||||
local time = game.tick + ticks
|
||||
local callback = {time = time, func_token = func_token, params = params}
|
||||
PriorityQueue.push(global.callbacks, callback, comp)
|
||||
end
|
||||
|
||||
--- Allows you to set a timer (in seconds) after which the tokened function will be run with params given as an argument
|
||||
-- Cannot be called before init
|
||||
-- @param sec <number>
|
||||
-- @param func_token <number> a token for a function store via the token system
|
||||
-- @param params <any> the argument to send to the tokened function
|
||||
function Task.set_timeout(sec, func_token, params)
|
||||
if not game then
|
||||
error('cannot call when game is not available', 2)
|
||||
end
|
||||
Task.set_timeout_in_ticks(60 * sec, func_token, params)
|
||||
end
|
||||
|
||||
--- Queueing allows you to split up heavy tasks which don't need to be completed in the same tick.
|
||||
-- Queued tasks are generally run 1 per tick. If the queue backs up, more tasks will be processed per tick.
|
||||
-- @param func_token <number> a token for a function stored via the token system
|
||||
-- If this function returns `true` it will run again the next tick, delaying other queued tasks (see weight)
|
||||
-- @param params <any> the argument to send to the tokened function
|
||||
-- @param weight <number> (defaults to 1) weight is the number of ticks a task is expected to take.
|
||||
-- Ex. if the task is expected to repeat multiple times (ie. the function returns true and loops several ticks)
|
||||
function Task.queue_task(func_token, params, weight)
|
||||
weight = weight or 1
|
||||
global.total_task_weight = global.total_task_weight + weight
|
||||
Queue.push(global.task_queue, {func_token = func_token, params = params, weight = weight})
|
||||
end
|
||||
|
||||
Event.add(defines.events.on_tick, on_tick)
|
||||
|
||||
return Task
|
||||
152
utils/timestamp.lua
Normal file
152
utils/timestamp.lua
Normal file
@@ -0,0 +1,152 @@
|
||||
--- 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 yday = months_to_days_cumulative[month]
|
||||
if month > 2 and is_leap(year) then
|
||||
yday = yday + 1
|
||||
end
|
||||
return yday + 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 radixes 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 sec<number> unix epoch timestamp
|
||||
-- @return {year: number, month: number, day: number, hour: number, min: number, sec: number}
|
||||
function Public.to_timetable(secs)
|
||||
return normalise(1970, 1, 1, 0, 0, secs)
|
||||
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 secs<type> unix epoch timestamp
|
||||
-- @return string
|
||||
function Public.to_string(secs)
|
||||
local tt = normalise(1970, 1, 1, 0, 0, secs)
|
||||
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