Skip to content

Instantly share code, notes, and snippets.

@d-lua-stuff
Last active January 11, 2021 11:28
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save d-lua-stuff/94bd1ae6966fb2c7925a1c126eb698a3 to your computer and use it in GitHub Desktop.
Save d-lua-stuff/94bd1ae6966fb2c7925a1c126eb698a3 to your computer and use it in GitHub Desktop.
Script packager/configurator for Dual Universe. See https://board.dualthegame.com/index.php?/topic/20161-lua-tool-script-packagerconfigurator-wraplua/ for more info. License: WTFPL
--------------------------------------------------------------------------------
-- wrap.lua bundle begins
-- version: 2020-09-04 6bd9a87
-- content sha256: 3f459084f0
--------------------------------------------------------------------------------
__lbs__version = "2020-09-04 6bd9a87"
do
do
local _ENV = _ENV
package.preload[ "argparse" ] = function( ... ) _ENV = _ENV;
-- The MIT License (MIT)
-- Copyright (c) 2013 - 2015 Peter Melnichenko
-- 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 function deep_update(t1, t2)
for k, v in pairs(t2) do
if type(v) == "table" then
v = deep_update({}, v)
end
t1[k] = v
end
return t1
end
-- A property is a tuple {name, callback}.
-- properties.args is number of properties that can be set as arguments
-- when calling an object.
local function class(prototype, properties, parent)
-- Class is the metatable of its instances.
local cl = {}
cl.__index = cl
if parent then
cl.__prototype = deep_update(deep_update({}, parent.__prototype), prototype)
else
cl.__prototype = prototype
end
if properties then
local names = {}
-- Create setter methods and fill set of property names.
for _, property in ipairs(properties) do
local name, callback = property[1], property[2]
cl[name] = function(self, value)
if not callback(self, value) then
self["_" .. name] = value
end
return self
end
names[name] = true
end
function cl.__call(self, ...)
-- When calling an object, if the first argument is a table,
-- interpret keys as property names, else delegate arguments
-- to corresponding setters in order.
if type((...)) == "table" then
for name, value in pairs((...)) do
if names[name] then
self[name](self, value)
end
end
else
local nargs = select("#", ...)
for i, property in ipairs(properties) do
if i > nargs or i > properties.args then
break
end
local arg = select(i, ...)
if arg ~= nil then
self[property[1]](self, arg)
end
end
end
return self
end
end
-- If indexing class fails, fallback to its parent.
local class_metatable = {}
class_metatable.__index = parent
function class_metatable.__call(self, ...)
-- Calling a class returns its instance.
-- Arguments are delegated to the instance.
local object = deep_update({}, self.__prototype)
setmetatable(object, self)
return object(...)
end
return setmetatable(cl, class_metatable)
end
local function typecheck(name, types, value)
for _, type_ in ipairs(types) do
if type(value) == type_ then
return true
end
end
error(("bad property '%s' (%s expected, got %s)"):format(name, table.concat(types, " or "), type(value)))
end
local function typechecked(name, ...)
local types = {...}
return {name, function(_, value) typecheck(name, types, value) end}
end
local multiname = {"name", function(self, value)
typecheck("name", {"string"}, value)
for alias in value:gmatch("%S+") do
self._name = self._name or alias
table.insert(self._aliases, alias)
end
-- Do not set _name as with other properties.
return true
end}
local function parse_boundaries(str)
if tonumber(str) then
return tonumber(str), tonumber(str)
end
if str == "*" then
return 0, math.huge
end
if str == "+" then
return 1, math.huge
end
if str == "?" then
return 0, 1
end
if str:match "^%d+%-%d+$" then
local min, max = str:match "^(%d+)%-(%d+)$"
return tonumber(min), tonumber(max)
end
if str:match "^%d+%+$" then
local min = str:match "^(%d+)%+$"
return tonumber(min), math.huge
end
end
local function boundaries(name)
return {name, function(self, value)
typecheck(name, {"number", "string"}, value)
local min, max = parse_boundaries(value)
if not min then
error(("bad property '%s'"):format(name))
end
self["_min" .. name], self["_max" .. name] = min, max
end}
end
local actions = {}
local option_action = {"action", function(_, value)
typecheck("action", {"function", "string"}, value)
if type(value) == "string" and not actions[value] then
error(("unknown action '%s'"):format(value))
end
end}
local option_init = {"init", function(self)
self._has_init = true
end}
local option_default = {"default", function(self, value)
if type(value) ~= "string" then
self._init = value
self._has_init = true
return true
end
end}
local add_help = {"add_help", function(self, value)
typecheck("add_help", {"boolean", "string", "table"}, value)
if self._has_help then
table.remove(self._options)
self._has_help = false
end
if value then
local help = self:flag()
:description "Show this help message and exit."
:action(function()
print(self:get_help())
os.exit(0)
end)
if value ~= true then
help = help(value)
end
if not help._name then
help "-h" "--help"
end
self._has_help = true
end
end}
local Parser = class({
_arguments = {},
_options = {},
_commands = {},
_mutexes = {},
_require_command = true,
_handle_options = true
}, {
args = 3,
typechecked("name", "string"),
typechecked("description", "string"),
typechecked("epilog", "string"),
typechecked("usage", "string"),
typechecked("help", "string"),
typechecked("require_command", "boolean"),
typechecked("handle_options", "boolean"),
typechecked("action", "function"),
typechecked("command_target", "string"),
add_help
})
local Command = class({
_aliases = {}
}, {
args = 3,
multiname,
typechecked("description", "string"),
typechecked("epilog", "string"),
typechecked("target", "string"),
typechecked("usage", "string"),
typechecked("help", "string"),
typechecked("require_command", "boolean"),
typechecked("handle_options", "boolean"),
typechecked("action", "function"),
typechecked("command_target", "string"),
add_help
}, Parser)
local Argument = class({
_minargs = 1,
_maxargs = 1,
_mincount = 1,
_maxcount = 1,
_defmode = "unused",
_show_default = true
}, {
args = 5,
typechecked("name", "string"),
typechecked("description", "string"),
option_default,
typechecked("convert", "function", "table"),
boundaries("args"),
typechecked("target", "string"),
typechecked("defmode", "string"),
typechecked("show_default", "boolean"),
typechecked("argname", "string", "table"),
option_action,
option_init
})
local Option = class({
_aliases = {},
_mincount = 0,
_overwrite = true
}, {
args = 6,
multiname,
typechecked("description", "string"),
option_default,
typechecked("convert", "function", "table"),
boundaries("args"),
boundaries("count"),
typechecked("target", "string"),
typechecked("defmode", "string"),
typechecked("show_default", "boolean"),
typechecked("overwrite", "boolean"),
typechecked("argname", "string", "table"),
option_action,
option_init
}, Argument)
function Argument:_get_argument_list()
local buf = {}
local i = 1
while i <= math.min(self._minargs, 3) do
local argname = self:_get_argname(i)
if self._default and self._defmode:find "a" then
argname = "[" .. argname .. "]"
end
table.insert(buf, argname)
i = i+1
end
while i <= math.min(self._maxargs, 3) do
table.insert(buf, "[" .. self:_get_argname(i) .. "]")
i = i+1
if self._maxargs == math.huge then
break
end
end
if i < self._maxargs then
table.insert(buf, "...")
end
return buf
end
function Argument:_get_usage()
local usage = table.concat(self:_get_argument_list(), " ")
if self._default and self._defmode:find "u" then
if self._maxargs > 1 or (self._minargs == 1 and not self._defmode:find "a") then
usage = "[" .. usage .. "]"
end
end
return usage
end
function actions.store_true(result, target)
result[target] = true
end
function actions.store_false(result, target)
result[target] = false
end
function actions.store(result, target, argument)
result[target] = argument
end
function actions.count(result, target, _, overwrite)
if not overwrite then
result[target] = result[target] + 1
end
end
function actions.append(result, target, argument, overwrite)
result[target] = result[target] or {}
table.insert(result[target], argument)
if overwrite then
table.remove(result[target], 1)
end
end
function actions.concat(result, target, arguments, overwrite)
if overwrite then
error("'concat' action can't handle too many invocations")
end
result[target] = result[target] or {}
for _, argument in ipairs(arguments) do
table.insert(result[target], argument)
end
end
function Argument:_get_action()
local action, init
if self._maxcount == 1 then
if self._maxargs == 0 then
action, init = "store_true", nil
else
action, init = "store", nil
end
else
if self._maxargs == 0 then
action, init = "count", 0
else
action, init = "append", {}
end
end
if self._action then
action = self._action
end
if self._has_init then
init = self._init
end
if type(action) == "string" then
action = actions[action]
end
return action, init
end
-- Returns placeholder for `narg`-th argument.
function Argument:_get_argname(narg)
local argname = self._argname or self:_get_default_argname()
if type(argname) == "table" then
return argname[narg]
else
return argname
end
end
function Argument:_get_default_argname()
return "<" .. self._name .. ">"
end
function Option:_get_default_argname()
return "<" .. self:_get_default_target() .. ">"
end
-- Returns label to be shown in the help message.
function Argument:_get_label()
return self._name
end
function Option:_get_label()
local variants = {}
local argument_list = self:_get_argument_list()
table.insert(argument_list, 1, nil)
for _, alias in ipairs(self._aliases) do
argument_list[1] = alias
table.insert(variants, table.concat(argument_list, " "))
end
return table.concat(variants, ", ")
end
function Command:_get_label()
return table.concat(self._aliases, ", ")
end
function Argument:_get_description()
if self._default and self._show_default then
if self._description then
return ("%s (default: %s)"):format(self._description, self._default)
else
return ("default: %s"):format(self._default)
end
else
return self._description or ""
end
end
function Command:_get_description()
return self._description or ""
end
function Option:_get_usage()
local usage = self:_get_argument_list()
table.insert(usage, 1, self._name)
usage = table.concat(usage, " ")
if self._mincount == 0 or self._default then
usage = "[" .. usage .. "]"
end
return usage
end
function Argument:_get_default_target()
return self._name
end
function Option:_get_default_target()
local res
for _, alias in ipairs(self._aliases) do
if alias:sub(1, 1) == alias:sub(2, 2) then
res = alias:sub(3)
break
end
end
res = res or self._name:sub(2)
return (res:gsub("-", "_"))
end
function Option:_is_vararg()
return self._maxargs ~= self._minargs
end
function Parser:_get_fullname()
local parent = self._parent
local buf = {self._name}
while parent do
table.insert(buf, 1, parent._name)
parent = parent._parent
end
return table.concat(buf, " ")
end
function Parser:_update_charset(charset)
charset = charset or {}
for _, command in ipairs(self._commands) do
command:_update_charset(charset)
end
for _, option in ipairs(self._options) do
for _, alias in ipairs(option._aliases) do
charset[alias:sub(1, 1)] = true
end
end
return charset
end
function Parser:argument(...)
local argument = Argument(...)
table.insert(self._arguments, argument)
return argument
end
function Parser:option(...)
local option = Option(...)
if self._has_help then
table.insert(self._options, #self._options, option)
else
table.insert(self._options, option)
end
return option
end
function Parser:flag(...)
return self:option():args(0)(...)
end
function Parser:command(...)
local command = Command():add_help(true)(...)
command._parent = self
table.insert(self._commands, command)
return command
end
function Parser:mutex(...)
local options = {...}
for i, option in ipairs(options) do
assert(getmetatable(option) == Option, ("bad argument #%d to 'mutex' (Option expected)"):format(i))
end
table.insert(self._mutexes, options)
return self
end
local max_usage_width = 70
local usage_welcome = "Usage: "
function Parser:get_usage()
if self._usage then
return self._usage
end
local lines = {usage_welcome .. self:_get_fullname()}
local function add(s)
if #lines[#lines]+1+#s <= max_usage_width then
lines[#lines] = lines[#lines] .. " " .. s
else
lines[#lines+1] = (" "):rep(#usage_welcome) .. s
end
end
-- This can definitely be refactored into something cleaner
local mutex_options = {}
local vararg_mutexes = {}
-- First, put mutexes which do not contain vararg options and remember those which do
for _, mutex in ipairs(self._mutexes) do
local buf = {}
local is_vararg = false
for _, option in ipairs(mutex) do
if option:_is_vararg() then
is_vararg = true
end
table.insert(buf, option:_get_usage())
mutex_options[option] = true
end
local repr = "(" .. table.concat(buf, " | ") .. ")"
if is_vararg then
table.insert(vararg_mutexes, repr)
else
add(repr)
end
end
-- Second, put regular options
for _, option in ipairs(self._options) do
if not mutex_options[option] and not option:_is_vararg() then
add(option:_get_usage())
end
end
-- Put positional arguments
for _, argument in ipairs(self._arguments) do
add(argument:_get_usage())
end
-- Put mutexes containing vararg options
for _, mutex_repr in ipairs(vararg_mutexes) do
add(mutex_repr)
end
for _, option in ipairs(self._options) do
if not mutex_options[option] and option:_is_vararg() then
add(option:_get_usage())
end
end
if #self._commands > 0 then
if self._require_command then
add("<command>")
else
add("[<command>]")
end
add("...")
end
return table.concat(lines, "\n")
end
local margin_len = 3
local margin_len2 = 25
local margin = (" "):rep(margin_len)
local margin2 = (" "):rep(margin_len2)
local function make_two_columns(s1, s2)
if s2 == "" then
return margin .. s1
end
s2 = s2:gsub("\n", "\n" .. margin2)
if #s1 < (margin_len2-margin_len) then
return margin .. s1 .. (" "):rep(margin_len2-margin_len-#s1) .. s2
else
return margin .. s1 .. "\n" .. margin2 .. s2
end
end
function Parser:get_help()
if self._help then
return self._help
end
local blocks = {self:get_usage()}
if self._description then
table.insert(blocks, self._description)
end
local labels = {"Arguments:", "Options:", "Commands:"}
for i, elements in ipairs{self._arguments, self._options, self._commands} do
if #elements > 0 then
local buf = {labels[i]}
for _, element in ipairs(elements) do
table.insert(buf, make_two_columns(element:_get_label(), element:_get_description()))
end
table.insert(blocks, table.concat(buf, "\n"))
end
end
if self._epilog then
table.insert(blocks, self._epilog)
end
return table.concat(blocks, "\n\n")
end
local function get_tip(context, wrong_name)
local context_pool = {}
local possible_name
local possible_names = {}
for name in pairs(context) do
if type(name) == "string" then
for i = 1, #name do
possible_name = name:sub(1, i - 1) .. name:sub(i + 1)
if not context_pool[possible_name] then
context_pool[possible_name] = {}
end
table.insert(context_pool[possible_name], name)
end
end
end
for i = 1, #wrong_name + 1 do
possible_name = wrong_name:sub(1, i - 1) .. wrong_name:sub(i + 1)
if context[possible_name] then
possible_names[possible_name] = true
elseif context_pool[possible_name] then
for _, name in ipairs(context_pool[possible_name]) do
possible_names[name] = true
end
end
end
local first = next(possible_names)
if first then
if next(possible_names, first) then
local possible_names_arr = {}
for name in pairs(possible_names) do
table.insert(possible_names_arr, "'" .. name .. "'")
end
table.sort(possible_names_arr)
return "\nDid you mean one of these: " .. table.concat(possible_names_arr, " ") .. "?"
else
return "\nDid you mean '" .. first .. "'?"
end
else
return ""
end
end
local ElementState = class({
invocations = 0
})
function ElementState:__call(state, element)
self.state = state
self.result = state.result
self.element = element
self.target = element._target or element:_get_default_target()
self.action, self.result[self.target] = element:_get_action()
return self
end
function ElementState:error(fmt, ...)
self.state:error(fmt, ...)
end
function ElementState:convert(argument)
local converter = self.element._convert
if converter then
local ok, err
if type(converter) == "function" then
ok, err = converter(argument)
else
ok = converter[argument]
end
if ok == nil then
self:error(err and "%s" or "malformed argument '%s'", err or argument)
end
argument = ok
end
return argument
end
function ElementState:default(mode)
return self.element._defmode:find(mode) and self.element._default
end
local function bound(noun, min, max, is_max)
local res = ""
if min ~= max then
res = "at " .. (is_max and "most" or "least") .. " "
end
local number = is_max and max or min
return res .. tostring(number) .. " " .. noun .. (number == 1 and "" or "s")
end
function ElementState:invoke(alias)
self.open = true
self.name = ("%s '%s'"):format(alias and "option" or "argument", alias or self.element._name)
self.overwrite = false
if self.invocations >= self.element._maxcount then
if self.element._overwrite then
self.overwrite = true
else
self:error("%s must be used %s", self.name, bound("time", self.element._mincount, self.element._maxcount, true))
end
else
self.invocations = self.invocations + 1
end
self.args = {}
if self.element._maxargs <= 0 then
self:close()
end
return self.open
end
function ElementState:pass(argument)
argument = self:convert(argument)
table.insert(self.args, argument)
if #self.args >= self.element._maxargs then
self:close()
end
return self.open
end
function ElementState:complete_invocation()
while #self.args < self.element._minargs do
self:pass(self.element._default)
end
end
function ElementState:close()
if self.open then
self.open = false
if #self.args < self.element._minargs then
if self:default("a") then
self:complete_invocation()
else
if #self.args == 0 then
if getmetatable(self.element) == Argument then
self:error("missing %s", self.name)
elseif self.element._maxargs == 1 then
self:error("%s requires an argument", self.name)
end
end
self:error("%s requires %s", self.name, bound("argument", self.element._minargs, self.element._maxargs))
end
end
local args = self.args
if self.element._maxargs <= 1 then
args = args[1]
end
if self.element._maxargs == 1 and self.element._minargs == 0 and self.element._mincount ~= self.element._maxcount then
args = self.args
end
self.action(self.result, self.target, args, self.overwrite)
end
end
local ParseState = class({
result = {},
options = {},
arguments = {},
argument_i = 1,
element_to_mutexes = {},
mutex_to_used_option = {},
command_actions = {}
})
function ParseState:__call(parser, error_handler)
self.parser = parser
self.error_handler = error_handler
self.charset = parser:_update_charset()
self:switch(parser)
return self
end
function ParseState:error(fmt, ...)
self.error_handler(self.parser, fmt:format(...))
end
function ParseState:switch(parser)
self.parser = parser
if parser._action then
table.insert(self.command_actions, {action = parser._action, name = parser._name})
end
for _, option in ipairs(parser._options) do
option = ElementState(self, option)
table.insert(self.options, option)
for _, alias in ipairs(option.element._aliases) do
self.options[alias] = option
end
end
for _, mutex in ipairs(parser._mutexes) do
for _, option in ipairs(mutex) do
if not self.element_to_mutexes[option] then
self.element_to_mutexes[option] = {}
end
table.insert(self.element_to_mutexes[option], mutex)
end
end
for _, argument in ipairs(parser._arguments) do
argument = ElementState(self, argument)
table.insert(self.arguments, argument)
argument:invoke()
end
self.handle_options = parser._handle_options
self.argument = self.arguments[self.argument_i]
self.commands = parser._commands
for _, command in ipairs(self.commands) do
for _, alias in ipairs(command._aliases) do
self.commands[alias] = command
end
end
end
function ParseState:get_option(name)
local option = self.options[name]
if not option then
self:error("unknown option '%s'%s", name, get_tip(self.options, name))
else
return option
end
end
function ParseState:get_command(name)
local command = self.commands[name]
if not command then
if #self.commands > 0 then
self:error("unknown command '%s'%s", name, get_tip(self.commands, name))
else
self:error("too many arguments")
end
else
return command
end
end
function ParseState:invoke(option, name)
self:close()
if self.element_to_mutexes[option.element] then
for _, mutex in ipairs(self.element_to_mutexes[option.element]) do
local used_option = self.mutex_to_used_option[mutex]
if used_option and used_option ~= option then
self:error("option '%s' can not be used together with %s", name, used_option.name)
else
self.mutex_to_used_option[mutex] = option
end
end
end
if option:invoke(name) then
self.option = option
end
end
function ParseState:pass(arg)
if self.option then
if not self.option:pass(arg) then
self.option = nil
end
elseif self.argument then
if not self.argument:pass(arg) then
self.argument_i = self.argument_i + 1
self.argument = self.arguments[self.argument_i]
end
else
local command = self:get_command(arg)
self.result[command._target or command._name] = true
if self.parser._command_target then
self.result[self.parser._command_target] = command._name
end
self:switch(command)
end
end
function ParseState:close()
if self.option then
self.option:close()
self.option = nil
end
end
function ParseState:finalize()
self:close()
for i = self.argument_i, #self.arguments do
local argument = self.arguments[i]
if #argument.args == 0 and argument:default("u") then
argument:complete_invocation()
else
argument:close()
end
end
if self.parser._require_command and #self.commands > 0 then
self:error("a command is required")
end
for _, option in ipairs(self.options) do
local name = option.name or ("option '%s'"):format(option.element._name)
if option.invocations == 0 then
if option:default("u") then
option:invoke(name)
option:complete_invocation()
option:close()
end
end
local mincount = option.element._mincount
if option.invocations < mincount then
if option:default("a") then
while option.invocations < mincount do
option:invoke(name)
option:close()
end
elseif option.invocations == 0 then
self:error("missing %s", name)
else
self:error("%s must be used %s", name, bound("time", mincount, option.element._maxcount))
end
end
end
for i = #self.command_actions, 1, -1 do
self.command_actions[i].action(self.result, self.command_actions[i].name)
end
end
function ParseState:parse(args)
for _, arg in ipairs(args) do
local plain = true
if self.handle_options then
local first = arg:sub(1, 1)
if self.charset[first] then
if #arg > 1 then
plain = false
if arg:sub(2, 2) == first then
if #arg == 2 then
self:close()
self.handle_options = false
else
local equals = arg:find "="
if equals then
local name = arg:sub(1, equals - 1)
local option = self:get_option(name)
if option.element._maxargs <= 0 then
self:error("option '%s' does not take arguments", name)
end
self:invoke(option, name)
self:pass(arg:sub(equals + 1))
else
local option = self:get_option(arg)
self:invoke(option, arg)
end
end
else
for i = 2, #arg do
local name = first .. arg:sub(i, i)
local option = self:get_option(name)
self:invoke(option, name)
if i ~= #arg and option.element._maxargs > 0 then
self:pass(arg:sub(i + 1))
break
end
end
end
end
end
end
if plain then
self:pass(arg)
end
end
self:finalize()
return self.result
end
function Parser:error(msg)
io.stderr:write(("%s\n\nError: %s\n"):format(self:get_usage(), msg))
os.exit(1)
end
-- Compatibility with strict.lua and other checkers:
local default_cmdline = rawget(_G, "arg") or {}
function Parser:_parse(args, error_handler)
return ParseState(self, error_handler):parse(args or default_cmdline)
end
function Parser:parse(args)
return self:_parse(args, self.error)
end
local function xpcall_error_handler(err)
return tostring(err) .. "\noriginal " .. debug.traceback("", 2):sub(2)
end
function Parser:pparse(args)
local parse_error
local ok, result = xpcall(function()
return self:_parse(args, function(_, err)
parse_error = err
error(err, 0)
end)
end, xpcall_error_handler)
if ok then
return true, result
elseif not parse_error then
error(result, 0)
else
return false, parse_error
end
end
return function(...)
return Parser(default_cmdline[0]):add_help(true)(...)
end
end
end
do
local _ENV = _ENV
package.preload[ "common.utils.array.add" ] = function( ... ) _ENV = _ENV;
--- Creates a new array that has items from all argument arrays added in sequence.
--- concat would be a better name, but there already is table.concat that does something else.
---@generic TItem
---@vararg TItem[]
---@return TItem[]
local function add (...)
local result = {}
local arrays = { ... }
for arrayIndex = 1, #arrays do
local array = arrays[arrayIndex]
for elementIndex = 1, #array do
result[#result + 1] = array[elementIndex]
end
end
return result
end
return add
end
end
do
local _ENV = _ENV
package.preload[ "common.utils.array.findOneMatching" ] = function( ... ) _ENV = _ENV;
--- Returns the first item matching the predicate.
---@generic TItem
---@param items TItem[]
---@param predicate fun(item:TItem):boolean
---@return TItem|nil, number|nil
local function findOneMatching (items, predicate)
for index, item in ipairs(items) do
if predicate(item) then
return item, index
end
end
end
return findOneMatching
end
end
do
local _ENV = _ENV
package.preload[ "common.utils.array.map" ] = function( ... ) _ENV = _ENV;
-- https://en.wikibooks.org/wiki/Lua_Functional_Programming/Functions
---@generic TSource, TResult
---@param array TSource[]
---@param func fun(item:TSource,index:number):TResult
---@return TResult[]
local function map (array, func)
local new_array = {}
for index, value in ipairs(array) do
new_array[index] = func(value,index)
end
return new_array
end
return map
end
end
do
local _ENV = _ENV
package.preload[ "common.utils.compat.pairs" ] = function( ... ) _ENV = _ENV;
-- adds __pairs support to Lua 5.1
if not _ENV then
local realPairs = pairs
-- luacheck: ignore pairs
pairs = function (tbl)
local mt = getmetatable(tbl)
if type(mt) == "table" and mt.__pairs then
return mt.__pairs(tbl)
else
return realPairs(tbl)
end
end
end
end
end
do
local _ENV = _ENV
package.preload[ "common.utils.string.replaceVars" ] = function( ... ) _ENV = _ENV;
---@param str string
---@param vars table<string, string>
local function replaceVars (str, vars)
local result = str:gsub("({([^}]+)})", function (whole, key)
return vars[key] or whole
end)
return result
end
return replaceVars
end
end
do
local _ENV = _ENV
package.preload[ "common.utils.table.OrderedMap" ] = function( ... ) _ENV = _ENV;
-- Penlight's OrderedMap adds over 150 KB of Lua code. This class implements some of its functionality.
local getValueKey = require "common.utils.table.getValueKey"
local concat = table.concat
local function iterateOrderedMap (om, key)
if key == nil then
key = om._keys[1]
else
local keyIndex = getValueKey(om._keys, key)
if not keyIndex then return nil end
key = om._keys[keyIndex + 1]
end
return key, om[key]
end
local OrderedMap = {}
OrderedMap.__index = OrderedMap
function OrderedMap.new (t)
local self = setmetatable({ _keys = {} }, OrderedMap)
if t then self:update(t) end
return self
end
function OrderedMap:update (t)
if #t > 0 then
for _, pair in ipairs(t) do
local k, v = next(pair)
if not k then error("Empty pair (key is null).") end
self[k] = v
end
else
for k, v in pairs(t) do
self[k] = v
end
end
return self
end
function OrderedMap:__pairs ()
return iterateOrderedMap, self, nil
end
function OrderedMap._ipairs ()
error("Not implemented!")
end
function OrderedMap:__newindex (k, v)
local keys = self._keys
if not getValueKey(keys, v) then
keys[#keys + 1] = k
end
rawset(self, k, v)
end
function OrderedMap:__tostring () -- for debugging and unit testing
local keys = self._keys
local buf = { "{" }
for i = 1, #keys do
local k = keys[i]
local v = self[k]
buf[#buf + 1] = k
buf[#buf + 1] = "="
if type(v) == "number" then
buf[#buf + 1] = v
else
buf[#buf + 1] = '"' .. tostring(v) .. '"'
end
if i < #keys then
buf[#buf + 1] = ","
end
end
buf[#buf + 1] = "}"
return concat(buf)
end
return setmetatable({}, {
__call = function(_, t) return OrderedMap.new(t) end,
__index = OrderedMap
})
end
end
do
local _ENV = _ENV
package.preload[ "common.utils.table.assign" ] = function( ... ) _ENV = _ENV;
-- Copies values of source tables into the target table and returns the target table
-- (like JavaScript's Object.assign)
---@generic TTargetTable, TCopiedTable
---@param targetTable TTargetTable
---@vararg TCopiedTable
---@return TTargetTable | TCopiedTable
local function assign (targetTable, ...)
local sourceTables = { ... }
for i = 1, #sourceTables do
for key, value in pairs(sourceTables[i]) do
targetTable[key] = value
end
end
return targetTable
end
return assign
end
end
do
local _ENV = _ENV
package.preload[ "common.utils.table.getKeys" ] = function( ... ) _ENV = _ENV;
---@generic TKey
---@param tbl table<TKey, any>
---@return TKey[]
local function getKeys (tbl)
local keys = {}
for key, _ in pairs(tbl) do
keys[#keys + 1] = key
end
return keys
end
return getKeys
end
end
do
local _ENV = _ENV
package.preload[ "common.utils.table.getValueKey" ] = function( ... ) _ENV = _ENV;
---@generic TKey, TValue
---@param tbl table<TKey, TValue>
---@param valueToFind TValue
---@return TValue
local function getValueKey (tbl, valueToFind)
for key, value in pairs(tbl) do
if value == valueToFind then
return key
end
end
return nil
end
return getValueKey
end
end
do
local _ENV = _ENV
package.preload[ "dkjson" ] = function( ... ) _ENV = _ENV;
-- Module options:
local always_try_using_lpeg = true
local register_global_module_table = false
local global_module_name = 'json'
--[==[
David Kolf's JSON module for Lua 5.1/5.2
Version 2.5
For the documentation see the corresponding readme.txt or visit
<http://dkolf.de/src/dkjson-lua.fsl/>.
You can contact the author by sending an e-mail to 'david' at the
domain 'dkolf.de'.
Copyright (C) 2010-2013 David Heiko Kolf
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.
--]==]
-- global dependencies:
local pairs, type, tostring, tonumber, getmetatable, setmetatable, rawset =
pairs, type, tostring, tonumber, getmetatable, setmetatable, rawset
local error, require, pcall, select = error, require, pcall, select
local floor, huge = math.floor, math.huge
local strrep, gsub, strsub, strbyte, strchar, strfind, strlen, strformat =
string.rep, string.gsub, string.sub, string.byte, string.char,
string.find, string.len, string.format
local strmatch = string.match
local concat = table.concat
local json = { version = "dkjson 2.5" }
if register_global_module_table then
_G[global_module_name] = json
end
local _ENV = nil -- blocking globals in Lua 5.2
pcall (function()
-- Enable access to blocked metatables.
-- Don't worry, this module doesn't change anything in them.
local debmeta = require "debug".getmetatable
if debmeta then getmetatable = debmeta end
end)
json.null = setmetatable ({}, {
__tojson = function () return "null" end
})
local function isarray (tbl)
local max, n, arraylen = 0, 0, 0
for k,v in pairs (tbl) do
if k == 'n' and type(v) == 'number' then
arraylen = v
if v > max then
max = v
end
else
if type(k) ~= 'number' or k < 1 or floor(k) ~= k then
return false
end
if k > max then
max = k
end
n = n + 1
end
end
if max > 10 and max > arraylen and max > n * 2 then
return false -- don't create an array with too many holes
end
return true, max
end
local escapecodes = {
["\""] = "\\\"", ["\\"] = "\\\\", ["\b"] = "\\b", ["\f"] = "\\f",
["\n"] = "\\n", ["\r"] = "\\r", ["\t"] = "\\t"
}
local function escapeutf8 (uchar)
local value = escapecodes[uchar]
if value then
return value
end
local a, b, c, d = strbyte (uchar, 1, 4)
a, b, c, d = a or 0, b or 0, c or 0, d or 0
if a <= 0x7f then
value = a
elseif 0xc0 <= a and a <= 0xdf and b >= 0x80 then
value = (a - 0xc0) * 0x40 + b - 0x80
elseif 0xe0 <= a and a <= 0xef and b >= 0x80 and c >= 0x80 then
value = ((a - 0xe0) * 0x40 + b - 0x80) * 0x40 + c - 0x80
elseif 0xf0 <= a and a <= 0xf7 and b >= 0x80 and c >= 0x80 and d >= 0x80 then
value = (((a - 0xf0) * 0x40 + b - 0x80) * 0x40 + c - 0x80) * 0x40 + d - 0x80
else
return ""
end
if value <= 0xffff then
return strformat ("\\u%.4x", value)
elseif value <= 0x10ffff then
-- encode as UTF-16 surrogate pair
value = value - 0x10000
local highsur, lowsur = 0xD800 + floor (value/0x400), 0xDC00 + (value % 0x400)
return strformat ("\\u%.4x\\u%.4x", highsur, lowsur)
else
return ""
end
end
local function fsub (str, pattern, repl)
-- gsub always builds a new string in a buffer, even when no match
-- exists. First using find should be more efficient when most strings
-- don't contain the pattern.
if strfind (str, pattern) then
return gsub (str, pattern, repl)
else
return str
end
end
local function quotestring (value)
-- based on the regexp "escapable" in https://github.com/douglascrockford/JSON-js
value = fsub (value, "[%z\1-\31\"\\\127]", escapeutf8)
if strfind (value, "[\194\216\220\225\226\239]") then
value = fsub (value, "\194[\128-\159\173]", escapeutf8)
value = fsub (value, "\216[\128-\132]", escapeutf8)
value = fsub (value, "\220\143", escapeutf8)
value = fsub (value, "\225\158[\180\181]", escapeutf8)
value = fsub (value, "\226\128[\140-\143\168-\175]", escapeutf8)
value = fsub (value, "\226\129[\160-\175]", escapeutf8)
value = fsub (value, "\239\187\191", escapeutf8)
value = fsub (value, "\239\191[\176-\191]", escapeutf8)
end
return "\"" .. value .. "\""
end
json.quotestring = quotestring
local function replace(str, o, n)
local i, j = strfind (str, o, 1, true)
if i then
return strsub(str, 1, i-1) .. n .. strsub(str, j+1, -1)
else
return str
end
end
-- locale independent num2str and str2num functions
local decpoint, numfilter
local function updatedecpoint ()
decpoint = strmatch(tostring(0.5), "([^05+])")
-- build a filter that can be used to remove group separators
numfilter = "[^0-9%-%+eE" .. gsub(decpoint, "[%^%$%(%)%%%.%[%]%*%+%-%?]", "%%%0") .. "]+"
end
updatedecpoint()
local function num2str (num)
return replace(fsub(tostring(num), numfilter, ""), decpoint, ".")
end
local function str2num (str)
local num = tonumber(replace(str, ".", decpoint))
if not num then
updatedecpoint()
num = tonumber(replace(str, ".", decpoint))
end
return num
end
local function addnewline2 (level, buffer, buflen)
buffer[buflen+1] = "\n"
buffer[buflen+2] = strrep (" ", level)
buflen = buflen + 2
return buflen
end
function json.addnewline (state)
if state.indent then
state.bufferlen = addnewline2 (state.level or 0,
state.buffer, state.bufferlen or #(state.buffer))
end
end
local encode2 -- forward declaration
local function addpair (key, value, prev, indent, level, buffer, buflen, tables, globalorder, state)
local kt = type (key)
if kt ~= 'string' and kt ~= 'number' then
return nil, "type '" .. kt .. "' is not supported as a key by JSON."
end
if prev then
buflen = buflen + 1
buffer[buflen] = ","
end
if indent then
buflen = addnewline2 (level, buffer, buflen)
end
buffer[buflen+1] = quotestring (key)
buffer[buflen+2] = ":"
return encode2 (value, indent, level, buffer, buflen + 2, tables, globalorder, state)
end
local function appendcustom(res, buffer, state)
local buflen = state.bufferlen
if type (res) == 'string' then
buflen = buflen + 1
buffer[buflen] = res
end
return buflen
end
local function exception(reason, value, state, buffer, buflen, defaultmessage)
defaultmessage = defaultmessage or reason
local handler = state.exception
if not handler then
return nil, defaultmessage
else
state.bufferlen = buflen
local ret, msg = handler (reason, value, state, defaultmessage)
if not ret then return nil, msg or defaultmessage end
return appendcustom(ret, buffer, state)
end
end
function json.encodeexception(reason, value, state, defaultmessage)
return quotestring("<" .. defaultmessage .. ">")
end
encode2 = function (value, indent, level, buffer, buflen, tables, globalorder, state)
local valtype = type (value)
local valmeta = getmetatable (value)
valmeta = type (valmeta) == 'table' and valmeta -- only tables
local valtojson = valmeta and valmeta.__tojson
if valtojson then
if tables[value] then
return exception('reference cycle', value, state, buffer, buflen)
end
tables[value] = true
state.bufferlen = buflen
local ret, msg = valtojson (value, state)
if not ret then return exception('custom encoder failed', value, state, buffer, buflen, msg) end
tables[value] = nil
buflen = appendcustom(ret, buffer, state)
elseif value == nil then
buflen = buflen + 1
buffer[buflen] = "null"
elseif valtype == 'number' then
local s
if value ~= value or value >= huge or -value >= huge then
-- This is the behaviour of the original JSON implementation.
s = "null"
else
s = num2str (value)
end
buflen = buflen + 1
buffer[buflen] = s
elseif valtype == 'boolean' then
buflen = buflen + 1
buffer[buflen] = value and "true" or "false"
elseif valtype == 'string' then
buflen = buflen + 1
buffer[buflen] = quotestring (value)
elseif valtype == 'table' then
if tables[value] then
return exception('reference cycle', value, state, buffer, buflen)
end
tables[value] = true
level = level + 1
local isa, n = isarray (value)
if n == 0 and valmeta and valmeta.__jsontype == 'object' then
isa = false
end
local msg
if isa then -- JSON array
buflen = buflen + 1
buffer[buflen] = "["
for i = 1, n do
buflen, msg = encode2 (value[i], indent, level, buffer, buflen, tables, globalorder, state)
if not buflen then return nil, msg end
if i < n then
buflen = buflen + 1
buffer[buflen] = ","
end
end
buflen = buflen + 1
buffer[buflen] = "]"
else -- JSON object
local prev = false
buflen = buflen + 1
buffer[buflen] = "{"
local order = valmeta and valmeta.__jsonorder or globalorder
if order then
local used = {}
n = #order
for i = 1, n do
local k = order[i]
local v = value[k]
if v then
used[k] = true
buflen, msg = addpair (k, v, prev, indent, level, buffer, buflen, tables, globalorder, state)
prev = true -- add a seperator before the next element
end
end
for k,v in pairs (value) do
if not used[k] then
buflen, msg = addpair (k, v, prev, indent, level, buffer, buflen, tables, globalorder, state)
if not buflen then return nil, msg end
prev = true -- add a seperator before the next element
end
end
else -- unordered
for k,v in pairs (value) do
buflen, msg = addpair (k, v, prev, indent, level, buffer, buflen, tables, globalorder, state)
if not buflen then return nil, msg end
prev = true -- add a seperator before the next element
end
end
if indent then
buflen = addnewline2 (level - 1, buffer, buflen)
end
buflen = buflen + 1
buffer[buflen] = "}"
end
tables[value] = nil
else
return exception ('unsupported type', value, state, buffer, buflen,
"type '" .. valtype .. "' is not supported by JSON.")
end
return buflen
end
function json.encode (value, state)
state = state or {}
local oldbuffer = state.buffer
local buffer = oldbuffer or {}
state.buffer = buffer
updatedecpoint()
local ret, msg = encode2 (value, state.indent, state.level or 0,
buffer, state.bufferlen or 0, state.tables or {}, state.keyorder, state)
if not ret then
error (msg, 2)
elseif oldbuffer == buffer then
state.bufferlen = ret
return true
else
state.bufferlen = nil
state.buffer = nil
return concat (buffer)
end
end
local function loc (str, where)
local line, pos, linepos = 1, 1, 0
while true do
pos = strfind (str, "\n", pos, true)
if pos and pos < where then
line = line + 1
linepos = pos
pos = pos + 1
else
break
end
end
return "line " .. line .. ", column " .. (where - linepos)
end
local function unterminated (str, what, where)
return nil, strlen (str) + 1, "unterminated " .. what .. " at " .. loc (str, where)
end
local function scanwhite (str, pos)
while true do
pos = strfind (str, "%S", pos)
if not pos then return nil end
local sub2 = strsub (str, pos, pos + 1)
if sub2 == "\239\187" and strsub (str, pos + 2, pos + 2) == "\191" then
-- UTF-8 Byte Order Mark
pos = pos + 3
elseif sub2 == "//" then
pos = strfind (str, "[\n\r]", pos + 2)
if not pos then return nil end
elseif sub2 == "/*" then
pos = strfind (str, "*/", pos + 2)
if not pos then return nil end
pos = pos + 2
else
return pos
end
end
end
local escapechars = {
["\""] = "\"", ["\\"] = "\\", ["/"] = "/", ["b"] = "\b", ["f"] = "\f",
["n"] = "\n", ["r"] = "\r", ["t"] = "\t"
}
local function unichar (value)
if value < 0 then
return nil
elseif value <= 0x007f then
return strchar (value)
elseif value <= 0x07ff then
return strchar (0xc0 + floor(value/0x40),
0x80 + (floor(value) % 0x40))
elseif value <= 0xffff then
return strchar (0xe0 + floor(value/0x1000),
0x80 + (floor(value/0x40) % 0x40),
0x80 + (floor(value) % 0x40))
elseif value <= 0x10ffff then
return strchar (0xf0 + floor(value/0x40000),
0x80 + (floor(value/0x1000) % 0x40),
0x80 + (floor(value/0x40) % 0x40),
0x80 + (floor(value) % 0x40))
else
return nil
end
end
local function scanstring (str, pos)
local lastpos = pos + 1
local buffer, n = {}, 0
while true do
local nextpos = strfind (str, "[\"\\]", lastpos)
if not nextpos then
return unterminated (str, "string", pos)
end
if nextpos > lastpos then
n = n + 1
buffer[n] = strsub (str, lastpos, nextpos - 1)
end
if strsub (str, nextpos, nextpos) == "\"" then
lastpos = nextpos + 1
break
else
local escchar = strsub (str, nextpos + 1, nextpos + 1)
local value
if escchar == "u" then
value = tonumber (strsub (str, nextpos + 2, nextpos + 5), 16)
if value then
local value2
if 0xD800 <= value and value <= 0xDBff then
-- we have the high surrogate of UTF-16. Check if there is a
-- low surrogate escaped nearby to combine them.
if strsub (str, nextpos + 6, nextpos + 7) == "\\u" then
value2 = tonumber (strsub (str, nextpos + 8, nextpos + 11), 16)
if value2 and 0xDC00 <= value2 and value2 <= 0xDFFF then
value = (value - 0xD800) * 0x400 + (value2 - 0xDC00) + 0x10000
else
value2 = nil -- in case it was out of range for a low surrogate
end
end
end
value = value and unichar (value)
if value then
if value2 then
lastpos = nextpos + 12
else
lastpos = nextpos + 6
end
end
end
end
if not value then
value = escapechars[escchar] or escchar
lastpos = nextpos + 2
end
n = n + 1
buffer[n] = value
end
end
if n == 1 then
return buffer[1], lastpos
elseif n > 1 then
return concat (buffer), lastpos
else
return "", lastpos
end
end
local scanvalue -- forward declaration
local function scantable (what, closechar, str, startpos, nullval, objectmeta, arraymeta)
local len = strlen (str)
local tbl, n = {}, 0
local pos = startpos + 1
if what == 'object' then
setmetatable (tbl, objectmeta)
else
setmetatable (tbl, arraymeta)
end
while true do
pos = scanwhite (str, pos)
if not pos then return unterminated (str, what, startpos) end
local char = strsub (str, pos, pos)
if char == closechar then
return tbl, pos + 1
end
local val1, err
val1, pos, err = scanvalue (str, pos, nullval, objectmeta, arraymeta)
if err then return nil, pos, err end
pos = scanwhite (str, pos)
if not pos then return unterminated (str, what, startpos) end
char = strsub (str, pos, pos)
if char == ":" then
if val1 == nil then
return nil, pos, "cannot use nil as table index (at " .. loc (str, pos) .. ")"
end
pos = scanwhite (str, pos + 1)
if not pos then return unterminated (str, what, startpos) end
local val2
val2, pos, err = scanvalue (str, pos, nullval, objectmeta, arraymeta)
if err then return nil, pos, err end
tbl[val1] = val2
pos = scanwhite (str, pos)
if not pos then return unterminated (str, what, startpos) end
char = strsub (str, pos, pos)
else
n = n + 1
tbl[n] = val1
end
if char == "," then
pos = pos + 1
end
end
end
scanvalue = function (str, pos, nullval, objectmeta, arraymeta)
pos = pos or 1
pos = scanwhite (str, pos)
if not pos then
return nil, strlen (str) + 1, "no valid JSON value (reached the end)"
end
local char = strsub (str, pos, pos)
if char == "{" then
return scantable ('object', "}", str, pos, nullval, objectmeta, arraymeta)
elseif char == "[" then
return scantable ('array', "]", str, pos, nullval, objectmeta, arraymeta)
elseif char == "\"" then
return scanstring (str, pos)
else
local pstart, pend = strfind (str, "^%-?[%d%.]+[eE]?[%+%-]?%d*", pos)
if pstart then
local number = str2num (strsub (str, pstart, pend))
if number then
return number, pend + 1
end
end
pstart, pend = strfind (str, "^%a%w*", pos)
if pstart then
local name = strsub (str, pstart, pend)
if name == "true" then
return true, pend + 1
elseif name == "false" then
return false, pend + 1
elseif name == "null" then
return nullval, pend + 1
end
end
return nil, pos, "no valid JSON value at " .. loc (str, pos)
end
end
local function optionalmetatables(...)
if select("#", ...) > 0 then
return ...
else
return {__jsontype = 'object'}, {__jsontype = 'array'}
end
end
function json.decode (str, pos, nullval, ...)
local objectmeta, arraymeta = optionalmetatables(...)
return scanvalue (str, pos, nullval, objectmeta, arraymeta)
end
function json.use_lpeg ()
local g = require ("lpeg")
if g.version() == "0.11" then
error "due to a bug in LPeg 0.11, it cannot be used for JSON matching"
end
local pegmatch = g.match
local P, S, R = g.P, g.S, g.R
local function ErrorCall (str, pos, msg, state)
if not state.msg then
state.msg = msg .. " at " .. loc (str, pos)
state.pos = pos
end
return false
end
local function Err (msg)
return g.Cmt (g.Cc (msg) * g.Carg (2), ErrorCall)
end
local SingleLineComment = P"//" * (1 - S"\n\r")^0
local MultiLineComment = P"/*" * (1 - P"*/")^0 * P"*/"
local Space = (S" \n\r\t" + P"\239\187\191" + SingleLineComment + MultiLineComment)^0
local PlainChar = 1 - S"\"\\\n\r"
local EscapeSequence = (P"\\" * g.C (S"\"\\/bfnrt" + Err "unsupported escape sequence")) / escapechars
local HexDigit = R("09", "af", "AF")
local function UTF16Surrogate (match, pos, high, low)
high, low = tonumber (high, 16), tonumber (low, 16)
if 0xD800 <= high and high <= 0xDBff and 0xDC00 <= low and low <= 0xDFFF then
return true, unichar ((high - 0xD800) * 0x400 + (low - 0xDC00) + 0x10000)
else
return false
end
end
local function UTF16BMP (hex)
return unichar (tonumber (hex, 16))
end
local U16Sequence = (P"\\u" * g.C (HexDigit * HexDigit * HexDigit * HexDigit))
local UnicodeEscape = g.Cmt (U16Sequence * U16Sequence, UTF16Surrogate) + U16Sequence/UTF16BMP
local Char = UnicodeEscape + EscapeSequence + PlainChar
local String = P"\"" * g.Cs (Char ^ 0) * (P"\"" + Err "unterminated string")
local Integer = P"-"^(-1) * (P"0" + (R"19" * R"09"^0))
local Fractal = P"." * R"09"^0
local Exponent = (S"eE") * (S"+-")^(-1) * R"09"^1
local Number = (Integer * Fractal^(-1) * Exponent^(-1))/str2num
local Constant = P"true" * g.Cc (true) + P"false" * g.Cc (false) + P"null" * g.Carg (1)
local SimpleValue = Number + String + Constant
local ArrayContent, ObjectContent
-- The functions parsearray and parseobject parse only a single value/pair
-- at a time and store them directly to avoid hitting the LPeg limits.
local function parsearray (str, pos, nullval, state)
local obj, cont
local npos
local t, nt = {}, 0
repeat
obj, cont, npos = pegmatch (ArrayContent, str, pos, nullval, state)
if not npos then break end
pos = npos
nt = nt + 1
t[nt] = obj
until cont == 'last'
return pos, setmetatable (t, state.arraymeta)
end
local function parseobject (str, pos, nullval, state)
local obj, key, cont
local npos
local t = {}
repeat
key, obj, cont, npos = pegmatch (ObjectContent, str, pos, nullval, state)
if not npos then break end
pos = npos
t[key] = obj
until cont == 'last'
return pos, setmetatable (t, state.objectmeta)
end
local Array = P"[" * g.Cmt (g.Carg(1) * g.Carg(2), parsearray) * Space * (P"]" + Err "']' expected")
local Object = P"{" * g.Cmt (g.Carg(1) * g.Carg(2), parseobject) * Space * (P"}" + Err "'}' expected")
local Value = Space * (Array + Object + SimpleValue)
local ExpectedValue = Value + Space * Err "value expected"
ArrayContent = Value * Space * (P"," * g.Cc'cont' + g.Cc'last') * g.Cp()
local Pair = g.Cg (Space * String * Space * (P":" + Err "colon expected") * ExpectedValue)
ObjectContent = Pair * Space * (P"," * g.Cc'cont' + g.Cc'last') * g.Cp()
local DecodeValue = ExpectedValue * g.Cp ()
function json.decode (str, pos, nullval, ...)
local state = {}
state.objectmeta, state.arraymeta = optionalmetatables(...)
local obj, retpos = pegmatch (DecodeValue, str, pos, nullval, state)
if state.msg then
return nil, state.pos, state.msg
else
return obj, retpos
end
end
-- use this function only once:
json.use_lpeg = function () return json end
json.using_lpeg = true
return json -- so you can get the module using json = require "dkjson".use_lpeg()
end
if always_try_using_lpeg then
pcall (json.use_lpeg)
end
return json
end
end
do
local _ENV = _ENV
package.preload[ "du.script-config.Autoconf" ] = function( ... ) _ENV = _ENV;
local findOneMatching = require "common.utils.array.findOneMatching"
local AutoconfSlot = require "du.script-config.AutoconfSlot"
local insert, unpack = table.insert, unpack or table.unpack -- luacheck: ignore unpack
---@param name string
local function createAutoconf (name)
name = name or "Unnamed autoconf"
local self = {} ---@class ScriptAutoconf
---@type ScriptAutoconfSlot[]
local slots = {
AutoconfSlot.new("unit", "control", nil, nil, true),
AutoconfSlot.new("system", "system", nil, nil, true),
AutoconfSlot.new("library", "library", nil, nil, true)
}
---@param slotName string
---@param slotTypeName string
---@param slotClass string
---@param slotSelect string|nil
function self.addSlot (slotName, slotTypeName, slotClass, slotSelect)
local slot = AutoconfSlot.new(slotName, slotTypeName, slotClass, slotSelect)
insert(slots, slot)
return slot
end
---@return ScriptAutoconfSlot[]
function self.getSlots ()
return { unpack(slots) }
end
---@param slotName string
function self.getSlotByName (slotName)
return findOneMatching(slots, function (slot) return slot.getName() == slotName end)
end
function self.getName ()
return name
end
---@param newName string
function self.setName (newName)
name = newName
end
return self
end
return { new = createAutoconf }
end
end
do
local _ENV = _ENV
package.preload[ "du.script-config.AutoconfSlot" ] = function( ... ) _ENV = _ENV;
local Handler = require "du.script-config.Handler"
local insert, unpack = table.insert, unpack or table.unpack -- luacheck: ignore unpack
local function createAutoconfSlot (name, typeName, class, select, isBuiltin)
local self = {} ---@class ScriptAutoconfSlot
local handlers = {} ---@type ScriptHandler[]
function self.getName ()
return name
end
function self.getTypeName ()
return typeName
end
function self.getClass ()
return class
end
function self.getSelect ()
return select
end
function self.getIsBuiltin ()
return isBuiltin
end
function self.addHandler (filterSignature, filterArguments, code)
local handler = Handler.new(filterSignature, filterArguments, code)
insert(handlers, handler)
return handler
end
---@return ScriptHandler[]
function self.getHandlers ()
return { unpack(handlers) }
end
return self
end
return { new = createAutoconfSlot }
end
end
do
local _ENV = _ENV
package.preload[ "du.script-config.Config" ] = function( ... ) _ENV = _ENV;
local map = require "common.utils.array.map"
local findOneMatching = require "common.utils.array.findOneMatching"
local ConfigMethod = require "du.script-config.ConfigMethod"
local ConfigSlot = require "du.script-config.ConfigSlot"
local insert, unpack = table.insert, unpack or table.unpack -- luacheck: ignore unpack
local function createConfig ()
local self = {} ---@class ScriptConfig
local slots = {} ---@type ScriptConfigSlot[]
local methods = {} ---@type ScriptConfigMethod[]
---@param slotKey number
---@param slotName string
---@param slotElementType string
function self.addSlot (slotKey, slotName, slotElementType)
local slot = ConfigSlot.new(slotKey, slotName, slotElementType)
insert(slots, slot)
return slot
end
---@param signature string
---@param code string
function self.addMethod (signature, code)
local method = ConfigMethod.new(signature, code)
insert(methods, method)
return method
end
---@return ScriptConfigSlot[]
function self.getSlots ()
return { unpack(slots) }
end
function self.getSlotKeys ()
return map(slots, function (slot) return slot.getKey() end)
end
function self.getSlotByKey (slotKey)
return findOneMatching(slots, function (slot) return slot.getKey() == slotKey end)
end
function self.getSlotByName (slotName)
return findOneMatching(slots, function (slot) return slot.getName() == slotName end)
end
---@return ScriptConfigMethod[]
function self.getMethods ()
return { unpack(methods) }
end
return self
end
local function createDefaultConfig ()
local config = createConfig()
for slotIndex = 1, 10 do
config.addSlot(slotIndex - 1, "slot" .. slotIndex)
end
config.addSlot(-1, "unit", "control")
config.addSlot(-2, "system", "system")
config.addSlot(-3, "library", "library")
return config
end
return { new = createConfig, default = createDefaultConfig }
end
end
do
local _ENV = _ENV
package.preload[ "du.script-config.ConfigMethod" ] = function( ... ) _ENV = _ENV;
---@param signature string
---@param code string
local function createConfigMethod (signature, code)
local self = {} ---@class ScriptConfigMethod
function self.getSignature ()
return signature
end
function self.getCode ()
return code
end
---@param newSignature string
function self.setSignature (newSignature)
signature = newSignature
end
---@param newCode string
function self.setCode (newCode)
code = newCode
end
return self
end
return { new = createConfigMethod }
end
end
do
local _ENV = _ENV
package.preload[ "du.script-config.ConfigSlot" ] = function( ... ) _ENV = _ENV;
local Handler = require "du.script-config.Handler"
local insert, unpack = table.insert, unpack or table.unpack -- luacheck: ignore unpack
---@param key number
---@param name string
---@param elementType string
local function createConfigSlot (key, name, elementType)
local self = {} ---@class ScriptConfigSlot
local handlers = {} ---@type ScriptHandler[]
function self.getKey ()
return key
end
function self.getName ()
return name
end
function self.getElementType ()
return elementType
end
---@param newName string
function self.setName (newName)
name = newName
end
---@param newElementType string
function self.setElementType (newElementType)
elementType = newElementType
end
---@param filterSignature string
---@param filterArguments string[]
---@param code string
function self.addHandler (filterSignature, filterArguments, code)
local handler = Handler.new(filterSignature, filterArguments, code)
insert(handlers, handler)
return handler
end
---@return ScriptHandler[]
function self.getHandlers ()
return { unpack(handlers) }
end
return self
end
return { new = createConfigSlot }
end
end
do
local _ENV = _ENV
package.preload[ "du.script-config.Handler" ] = function( ... ) _ENV = _ENV;
---@param filterSignature string
---@param filterArguments string[]
---@param code string
local function createHandler (filterSignature, filterArguments, code)
local self = {} ---@class ScriptHandler
function self.getFilterSignature ()
return filterSignature
end
function self.getFilterArguments ()
return filterArguments
end
function self.getCode ()
return code
end
---@param newFilterSignature string
function self.setFilterSignature (newFilterSignature)
filterSignature = newFilterSignature
end
---@param newFilterArguments string
function self.setFilterArguments (newFilterArguments)
filterArguments = newFilterArguments
end
---@param newCode string
function self.setCode (newCode)
code = newCode
end
return self
end
return { new = createHandler }
end
end
do
local _ENV = _ENV
package.preload[ "du.script-config.autoconfToObject" ] = function( ... ) _ENV = _ENV;
local getValueKey = require "common.utils.table.getValueKey"
local parseFilterSignature = require "du.script-config.parseFilterSignature"
local OrderedMap = require "common.utils.table.OrderedMap"
---@param slot ScriptAutoconfSlot
local function getSlotObject (slot)
local slotObj = OrderedMap { class = slot.getClass() }
if slot.getSelect() then slotObj.select = slot.getSelect() end
return slotObj
end
---@param slots ScriptAutoconfSlot[]
local function getSlotsObject (slots)
local slotsObj = OrderedMap()
for _, slot in ipairs(slots) do
if not slot.getIsBuiltin() then
slotsObj[slot.getName()] = getSlotObject(slot)
end
end
return slotsObj
end
---@param handler ScriptHandler
---@param hasWildcardArgs boolean
local function getHandlerObject (handler, hasWildcardArgs)
local handlerObj = OrderedMap()
local filterArguments = handler.getFilterArguments()
if #filterArguments > 0 and not hasWildcardArgs then
handlerObj.args = filterArguments
end
handlerObj.lua = handler.getCode()
return handlerObj
end
---@param slot ScriptAutoconfSlot
local function getHandlersObjectForSlot (slot)
local handlersObj = OrderedMap()
for _, handler in ipairs(slot.getHandlers()) do
local filterSignature = handler.getFilterSignature()
local hasWildcardArgs = getValueKey(handler.getFilterArguments(), "*")
local handlerKey
if hasWildcardArgs then
handlerKey = filterSignature
else
local eventName, _ = parseFilterSignature(filterSignature)
handlerKey = eventName
end
handlersObj[handlerKey] = getHandlerObject(handler, hasWildcardArgs)
end
return handlersObj
end
---@param slots ScriptAutoconfSlot[]
local function getHandlersObject (slots)
local handlersObj = OrderedMap()
for _, slot in ipairs(slots) do
if #slot.getHandlers() > 0 then
handlersObj[slot.getName()] = getHandlersObjectForSlot(slot)
end
end
return handlersObj
end
---@param autoconf ScriptAutoconf
local function autoconfToObject (autoconf)
return OrderedMap {
{ name = autoconf.getName() },
{ slots = getSlotsObject(autoconf.getSlots()) },
{ handlers = getHandlersObject(autoconf.getSlots()) }
}
end
return autoconfToObject
end
end
do
local _ENV = _ENV
package.preload[ "du.script-config.autoconfToYaml" ] = function( ... ) _ENV = _ENV;
local autoconfToObject = require "du.script-config.autoconfToObject"
local objectToYaml = require "du.script-config.objectToYaml"
return function (autoconf)
return objectToYaml(autoconfToObject(autoconf))
end
end
end
do
local _ENV = _ENV
package.preload[ "du.script-config.configToJson" ] = function( ... ) _ENV = _ENV;
local configToObject = require "du.script-config.configToObject"
local objectToJson = require "du.script-config.objectToJson"
return function (config, indent)
return objectToJson(configToObject(config), indent)
end
end
end
do
local _ENV = _ENV
package.preload[ "du.script-config.configToObject" ] = function( ... ) _ENV = _ENV;
local map = require "common.utils.array.map"
local insert = table.insert
---@param slot ScriptConfigSlot
local function slotToObject (slot)
return {
name = slot.getName(),
type = {
methods = {},
events = {}
},
_elementType = slot.getElementType()
}
end
---@param argument string
local function filterArgumentToObject(argument)
if argument == "*" then
return { variable = argument }
else
return { value = argument }
end
end
---@param handler ScriptHandler
---@param handlerKey string
---@param slotKey number
local function handlerToObject(handler, handlerKey, slotKey)
return {
key = handlerKey,
filter = {
slotKey = slotKey,
signature = handler.getFilterSignature(),
args = map(handler.getFilterArguments(), filterArgumentToObject)
},
code = handler.getCode()
}
end
---@param method ScriptConfigMethod
local function methodToObject(method)
return {
signature = method.getSignature(),
code = method.getCode()
}
end
---@param config ScriptConfig
local function configToObject (config)
local obj = {
slots = {},
handlers = {},
methods = {},
events = {}
}
local nextHandlerKey = 0
for _, slot in ipairs(config.getSlots()) do
obj.slots[tostring(slot.getKey())] = slotToObject(slot)
for _, handler in ipairs(slot.getHandlers()) do
insert(obj.handlers, handlerToObject(handler, tostring(nextHandlerKey), slot.getKey()))
nextHandlerKey = nextHandlerKey + 1
end
end
for _, method in ipairs(config.getMethods()) do
insert(obj.methods, methodToObject(method))
end
return obj
end
return configToObject
end
end
do
local _ENV = _ENV
package.preload[ "du.script-config.objectToJson" ] = function( ... ) _ENV = _ENV;
local json = require "dkjson"
local insert = table.insert
local function addNumericKeyOrder(keyorder)
for key = 0, 200 do
insert(keyorder, tostring(key))
end
for key = -1, -20, -1 do
insert(keyorder, tostring(key))
end
end
local function getJsonOptions(indent)
local options = {
indent = indent,
keyorder = {
"slots", "handlers", "methods", "events", -- root
"name", "type", -- slot
"slotKey", "signature", "args", -- handler filter
"key", "filter", "code" -- handler
}
}
addNumericKeyOrder(options.keyorder)
return options
end
return function (obj, indent)
return json.encode(obj, getJsonOptions(indent))
end
end
end
do
local _ENV = _ENV
package.preload[ "du.script-config.objectToYaml" ] = function( ... ) _ENV = _ENV;
-- Supports just enough of YAML syntax to write an Autoconf file. Won't work with most other objects.
local concat = table.concat
local rep = string.rep
local indent = ' '
local firstCharsOfQuotedStrings = [['"[]{}>|*&!%#`@,?:-]]
local function getIndent (indentLevel)
return rep(indent, indentLevel)
end
local function write (buffer, str)
buffer[#buffer + 1] = str
end
local function writeIndent (buffer, indentLevel)
write(buffer, getIndent(indentLevel))
end
local function writeSpace (buffer)
write(buffer, " ")
end
local function writeNewline (buffer)
write(buffer, "\n")
end
local function writeInlineString (buffer, str)
local firstChar = str:sub(1, 1)
local shouldQuote = firstCharsOfQuotedStrings:find(firstChar, 1, true)
if shouldQuote then write(buffer, "'") end
write(buffer, str)
if shouldQuote then write(buffer, "'") end
end
local function writeMultilineString (buffer, indentLevel, str)
writeIndent(buffer, indentLevel)
write(buffer, str:gsub("\n", "%0" .. getIndent(indentLevel)))
writeNewline(buffer)
end
local function writeInlineArray (buffer, arr)
write(buffer, "[")
for index, value in ipairs(arr) do
local valueType = type(value)
if valueType == "string" and not value:match("\n") then
writeInlineString(buffer, value)
else
error("Array cannot be written. Cannot write element of type " .. valueType .. ".")
end
if index < #value then
write(buffer, ",")
end
end
write(buffer, "]")
end
local function writeTable (buffer, indentLevel, tbl)
for key, value in pairs(tbl) do
writeIndent(buffer, indentLevel)
write(buffer, key .. ":")
local valueType = type(value)
if valueType == "table" then
if value[1] then
writeSpace(buffer)
writeInlineArray(buffer, value)
writeNewline(buffer)
else
writeNewline(buffer)
writeTable(buffer, indentLevel + 1, value)
end
elseif valueType == "string" then
if value:match("\n") then
write(buffer, " |")
writeNewline(buffer)
writeMultilineString(buffer, indentLevel + 1, value)
else
writeSpace(buffer)
writeInlineString(buffer, value)
writeNewline(buffer)
end
else
error("Table cannot be written. Cannot write value of type " .. valueType .. ".")
end
end
end
return function (obj)
local buffer = {}
writeTable(buffer, 0, obj)
return concat(buffer)
end
end
end
do
local _ENV = _ENV
package.preload[ "du.script-config.parseFilterSignature" ] = function( ... ) _ENV = _ENV;
local insert = table.insert
---@param filterSignature string
---@return string, string[]
local function parseFilterSignature (filterSignature)
local funName, funArgs = filterSignature:match("^([^(]+)%((.*)%)")
local argNames = {}
for argName in funArgs:gmatch("[^%s,]+") do insert(argNames, argName) end
return funName, argNames
end
return parseFilterSignature
end
end
do
local _ENV = _ENV
package.preload[ "wrap.fixes" ] = function( ... ) _ENV = _ENV;
return {
gc = [[
-- Garbage collection fix added by wrap.lua
do
-- Set GC pause. This more or less means by how many % memory use should increase before a garbage collection is started. Lua default is 200
local newPause = 110
local oldPause = collectgarbage("setpause", newPause)
if oldPause < newPause then
-- DU now has a different default GC pause which is even lower. Revert back to it.
collectgarbage("setpause", oldPause)
end
end
]],
require = [[
-- require() fix added by wrap.lua
package = package or {}
package.preload = package.preload or {}
package.loaded = package.loaded or {}
package.preload["__wrap_lua__require_test"] = function () return true end
if not require or not pcall(require, "__wrap_lua__require_test") then
function require (modname)
if package.loaded[modname] then
return package.loaded[modname]
end
if package.preload[modname] then
local mod = package.preload[modname]()
if mod == nil then mod = true end
package.loaded[modname] = mod
return mod
end
error("module '" .. modname .. "' not found in package.loaded or package.preload")
end
end
]]
}
end
end
do
local _ENV = _ENV
package.preload[ "wrap.getArgsForFilter" ] = function( ... ) _ENV = _ENV;
local map = require "common.utils.array.map"
local parseFilterSignature = require "du.script-config.parseFilterSignature"
---@param filterSignature string
local function getArgsForFilter (filterSignature)
local _, argNames = parseFilterSignature(filterSignature)
return map(argNames, function () return "*" end)
end
return getArgsForFilter
end
end
do
local _ENV = _ENV
package.preload[ "wrap.getEventHandlerCode" ] = function( ... ) _ENV = _ENV;
local add = require "common.utils.array.add"
local getHandlerCode = require "wrap.getHandlerCode"
local parseFilterSignature = require "du.script-config.parseFilterSignature"
---@param templateString string
---@param scriptObject string
---@param callOperator string
---@param slotName string
---@param filterSignature string
local function getEventHandlerCode (templateString, scriptObject, callOperator, slotName, filterSignature)
local eventName, argNames = parseFilterSignature(filterSignature)
local eventMethod = "on" .. eventName:gsub("^%l", string.upper)
local eventArgsWithElement = add(argNames, { slotName })
return getHandlerCode(templateString, scriptObject, callOperator, eventMethod, eventArgsWithElement)
end
return getEventHandlerCode
end
end
do
local _ENV = _ENV
package.preload[ "wrap.getHandlerCode" ] = function( ... ) _ENV = _ENV;
local replaceVars = require "common.utils.string.replaceVars"
local concat = table.concat
---@param templateString string
---@param scriptObject string
---@param callOperator string
---@param method string
---@param args string
local function getHandlerCode (templateString, scriptObject, callOperator, method, args)
local argsStr = concat(args, ",")
local argsStrForPcall = (callOperator == ":" and (scriptObject .. ",") or "") .. argsStr
return replaceVars(templateString, {
scriptObject = scriptObject,
callOperator = callOperator,
method = method,
argsStr = argsStr,
argsStrForPcall = argsStrForPcall
})
end
return getHandlerCode
end
end
do
local _ENV = _ENV
package.preload[ "wrap.getMethodCode" ] = function( ... ) _ENV = _ENV;
local getHandlerCode = require "wrap.getHandlerCode"
local parseFilterSignature = require "du.script-config.parseFilterSignature"
---@param templateString string
---@param scriptObject string
---@param callOperator string
---@param methodSignature string
local function getEventHandlerCode (templateString, scriptObject, callOperator, methodSignature)
local methodName, argNames = parseFilterSignature(methodSignature)
return getHandlerCode(templateString, scriptObject, callOperator, methodName, argNames)
end
return getEventHandlerCode
end
end
do
local _ENV = _ENV
package.preload[ "wrap.getSlotType" ] = function( ... ) _ENV = _ENV;
local slotTypes = require "wrap.slotTypes"
---@param requiredSlotTypeName string|nil case-insensitive slot type name
local function getSlotType (requiredSlotTypeName)
if not requiredSlotTypeName then return nil end
for slotTypeName, slotType in pairs(slotTypes) do
if slotTypeName:lower() == requiredSlotTypeName:lower() then
return slotType
end
end
error("unknown slot type '" .. requiredSlotTypeName .. "'")
end
return getSlotType
end
end
do
local _ENV = _ENV
package.preload[ "wrap.makeAutoconf" ] = function( ... ) _ENV = _ENV;
local assign = require "common.utils.table.assign"
local getArgsForFilter = require "wrap.getArgsForFilter"
local getEventHandlerCode = require "wrap.getEventHandlerCode"
local getSlotType = require "wrap.getSlotType"
local replaceVars = require "common.utils.string.replaceVars"
local Autoconf = require "du.script-config.Autoconf"
local format = string.format
return function (options)
local scriptName = options.scriptName
local startHandlerCode = options.startHandlerCode
local scriptObjectName = options.scriptObjectName
local callOperator = options.callOperator
local slotDefinitions = options.slotDefinitions
local defaultSlotDefinition = options.defaultSlotDefinition
local methodSignatures = options.methodSignatures
local template = options.template
if defaultSlotDefinition then
error("Default slot option cannot be used with YAML output.")
end
if #methodSignatures > 0 then
error("Methods option cannot be used with YAML output.")
end
local autoconf = Autoconf.new(scriptName)
local fullStartHandlerCode = replaceVars(template.startEventHandler, {
scriptObject = scriptObjectName,
startHandlerCode = startHandlerCode
})
local unitSlot = autoconf.getSlotByName("unit")
unitSlot.addHandler("start()", {}, fullStartHandlerCode)
for _, slotDefinition in ipairs(slotDefinitions) do
local slotName = slotDefinition.name
local slotTypeName = slotDefinition.type
local slotSelect = slotDefinition.select
local slotType = getSlotType(slotTypeName) or {}
if slotDefinition.class then
slotType = assign({}, slotType, { class = slotDefinition.class })
end
local slotClass = slotType.class or error(format("Slot type '%s' cannot be used with YAML output.", slotTypeName))
autoconf.addSlot(slotName, slotTypeName, slotClass, slotSelect)
end
for _, slot in ipairs(autoconf.getSlots()) do
local slotName = slot.getName()
local slotType = getSlotType(slot.getTypeName())
for _, filterSignature in ipairs(slotType and slotType.filters or {}) do
local filterArgs = getArgsForFilter(filterSignature)
local handlerCode = getEventHandlerCode(template.eventHandler, scriptObjectName, callOperator, slotName, filterSignature)
slot.addHandler(filterSignature, filterArgs, handlerCode)
end
end
return autoconf
end
end
end
do
local _ENV = _ENV
package.preload[ "wrap.makeConfig" ] = function( ... ) _ENV = _ENV;
local getArgsForFilter = require "wrap.getArgsForFilter"
local getEventHandlerCode = require "wrap.getEventHandlerCode"
local getMethodCode = require "wrap.getMethodCode"
local getSlotType = require "wrap.getSlotType"
local replaceVars = require "common.utils.string.replaceVars"
local Config = require "du.script-config.Config"
return function (options)
local startHandlerCode = options.startHandlerCode
local scriptObjectName = options.scriptObjectName
local callOperator = options.callOperator
local slotDefinitions = options.slotDefinitions
local defaultSlotDefinition = options.defaultSlotDefinition
local methodSignatures = options.methodSignatures
local template = options.template
local config = Config.default()
local fullStartHandlerCode = replaceVars(template.startEventHandler, {
scriptObject = scriptObjectName,
startHandlerCode = startHandlerCode
})
local unitSlot = config.getSlotByName("unit")
unitSlot.addHandler("start()", {}, fullStartHandlerCode)
for slotIndex, slotDefinition in ipairs(slotDefinitions) do
if slotDefinition.class then
error("Slot option key 'class' cannot be used with JSON output.")
end
if slotDefinition.select then
error("Slot option key 'select' cannot be used with JSON output.")
end
local slotName = slotDefinition.name
local slotTypeName = slotDefinition.type
local slot = config.getSlotByKey(slotIndex - 1)
slot.setName(slotName)
if slotTypeName then
slot.setElementType(slotTypeName)
end
end
for _, slot in ipairs(config.getSlots()) do
local slotName = slot.getName()
local slotTypeName = slot.getElementType() or defaultSlotDefinition and defaultSlotDefinition.type
local slotType = getSlotType(slotTypeName)
local filterSignatures = slotType and slotType.filters or {}
for _, filterSignature in ipairs(filterSignatures) do
local filterArgs = getArgsForFilter(filterSignature)
local handlerCode = getEventHandlerCode(template.eventHandler, scriptObjectName, callOperator, slotName, filterSignature)
slot.addHandler(filterSignature, filterArgs, handlerCode)
end
end
for _, methodSignature in ipairs(methodSignatures) do
local methodCode = getMethodCode(template.methodHandler, scriptObjectName, callOperator, methodSignature)
config.addMethod(methodSignature, methodCode)
end
return config
end
end
end
do
local _ENV = _ENV
package.preload[ "wrap.slotTypes" ] = function( ... ) _ENV = _ENV;
local add = require "common.utils.array.add"
local types = {
-- elements
antigravityGenerator = {
class = "AntiGravityGeneratorUnit"
},
core = {
class = "CoreUnit"
},
databank = {
class = "DataBank"
},
fuelContainer = {
class = "FuelContainer"
},
industry = {
class = "IndustryUnit",
filters = { "completed()", "statusChanged(status)" }
},
gyro = {
class = "GyroUnit"
},
radar = {
class = "RadarUnit"
},
pvpRadar = {
class = "RadarPVPUnit"
},
screen = {
class = "ScreenUnit",
filters = { "mouseDown(x,y)", "mouseUp(x,y)" }
},
laserDetector = {
class = "LaserDetectorUnit",
filters = { "laserHit()", "laserRelease()" }
},
receiver = {
class = "ReceiverUnit",
filters = { "receive(channel,message)" }
},
warpDrive = {
class = "WarpDriveUnit",
},
weapon = {
class = "WeaponUnit"
},
-- abstract
enterable = {
filters = { "enter(id)", "leave(id)" }
},
pressable = {
filters = { "pressed()", "released()" }
},
-- built-in
control = {
filters = { "stop()", "tick(timerId)" }
},
system = {
filters = { "actionStart(action)", "actionStop(action)", "actionLoop(action)", "update()", "flush()" },
},
library = {
}
}
types.anything = {
filters = add(
types.enterable.filters,
types.industry.filters,
types.pressable.filters,
types.laserDetector.filters,
types.receiver.filters,
types.screen.filters
)
}
types.pvpRadar.filters = types.enterable.filters
types.radar.filters = types.enterable.filters
return types
end
end
do
local _ENV = _ENV
package.preload[ "wrap.templates" ] = function( ... ) _ENV = _ENV;
return {
copy = function (template)
return {
eventHandler = template.eventHandler,
methodHandler = template.methodHandler,
startEventHandler = template.startEventHandler
}
end,
default = {
eventHandler = "if {scriptObject}.{method} then {scriptObject}{callOperator}{method}({argsStr}) end",
methodHandler = "if {scriptObject}.{method} then return {scriptObject}{callOperator}{method}({argsStr}) end",
startEventHandler = "{startHandlerCode}"
},
withErrorHandling = {
eventHandler = [[
if not __wrap_lua__stopped and {scriptObject}.{method} then
local ok, message = xpcall({scriptObject}.{method},__wrap_lua__traceback,{argsStrForPcall})
if not ok then __wrap_lua__error(message) end
end]],
methodHandler = [[
if {scriptObject}.{method} then
local ret = { xpcall({scriptObject}.{method},__wrap_lua__traceback,{argsStrForPcall}) }
if not ret[1] then __wrap_lua__error(ret[2]) end
return table.unpack(ret, 2)
end]],
startEventHandler = [=[
-- error handling code added by wrap.lua
__wrap_lua__stopped = false
__wrap_lua__stopOnError = false
__wrap_lua__rethrowErrorAlways = false
__wrap_lua__rethrowErrorIfStopped = true
__wrap_lua__printError = true
__wrap_lua__showErrorOnScreens = true
function __wrap_lua__error (message)
if __wrap_lua__stopped then return end
-- make the traceback more readable and escape HTML syntax characters
message = tostring(message):gsub('"%-%- |STDERROR%-EVENTHANDLER[^"]*"', 'chunk'):gsub("&", "&amp;"):gsub("<", "&lt;"):gsub(">", "&gt;")
local unit = unit or self or {}
if __wrap_lua__showErrorOnScreens then
for _, value in pairs(unit) do
if type(value) == "table" and value.setCenteredText and value.setHTML then -- value is a screen
if message:match("\n") then
value.setHTML([[
<pre style="color: white; background-color: black; font-family: Consolas,monospace; font-size: 4vh; white-space: pre-wrap; margin: 1em">
Error: ]] .. message .. [[
</pre>]])
else
value.setCenteredText(message)
end
end
end
end
if __wrap_lua__printError and system and system.print then
system.print("Error: " .. message:gsub("\n", "<br>"))
end
if __wrap_lua__stopOnError then
__wrap_lua__stopped = true
end
if __wrap_lua__stopped and unit and unit.exit then
unit.exit()
end
if __wrap_lua__rethrowErrorAlways or (__wrap_lua__stopped and __wrap_lua__rethrowErrorIfStopped) then
error(message)
end
end
-- in case traceback is removed or renamed
__wrap_lua__traceback = traceback or (debug and debug.traceback) or function (arg1, arg2) return arg2 or arg1 end
local ok, message = xpcall(function ()
-- script code
{startHandlerCode}
-- error handling code added by wrap.lua
end, __wrap_lua__traceback)
if not ok then
__wrap_lua__error(message)
if not {scriptObject} then {scriptObject} = {} end
end]=]
},
withErrorHandlingMinified = {
eventHandler = [[
if not __wrap_lua__stopped and {scriptObject}.{method} then
local a,b=xpcall({scriptObject}.{method},__wrap_lua__traceback,{argsStrForPcall})
if not a then __wrap_lua__error(b) end
end]],
methodHandler = [[
if {scriptObject}.{method} then
local a={xpcall({scriptObject}.{method},__wrap_lua__traceback,{argsStrForPcall})}
if not a[1] then __wrap_lua__error(a[2]) end
return table.unpack(a, 2)
end]],
startEventHandler = [=[
__wrap_lua__stopped=false
__wrap_lua__stopOnError=false
__wrap_lua__rethrowErrorAlways=false
__wrap_lua__rethrowErrorIfStopped=true
__wrap_lua__printError=true
__wrap_lua__showErrorOnScreens=true
function __wrap_lua__error(a)
if __wrap_lua__stopped then return end
a=tostring(a):gsub('"%-%- |STDERROR%-EVENTHANDLER[^"]*"','chunk'):gsub("&","&amp;"):gsub("<","&lt;"):gsub(">","&gt;")
local b=unit or self or {}
if __wrap_lua__showErrorOnScreens then for _,c in pairs(b) do if type(c)=="table" and c.setCenteredText and c.setHTML then if a:match("\n") then c.setHTML([[
<pre style="color: white; background-color: black; font-family: Consolas,monospace; font-size: 4vh; white-space: pre-wrap; margin: 1em">
Error: ]]..a..[[
</pre>]]) else c.setCenteredText(a) end end end end
if __wrap_lua__printError and system and system.print then system.print("Error: "..a:gsub("\n","<br>")) end
if __wrap_lua__stopOnError then __wrap_lua__stopped=true end
if __wrap_lua__stopped and b and b.exit then b.exit() end
if __wrap_lua__rethrowErrorAlways or (__wrap_lua__stopped and __wrap_lua__rethrowErrorIfStopped) then error(a) end
end
__wrap_lua__traceback=traceback or (debug and debug.traceback) or function(a,b)return b or a end
local a,b=xpcall(function()
{startHandlerCode}
end,__wrap_lua__traceback)
if not a then
__wrap_lua__error(b)
if not {scriptObject} then {scriptObject} = {} end
end
]=]
}
}
end
end
end
-- Lua script packager/configurator for DU
-- Copyright (C) 2020 D. L., a.k.a hdparm
require "common.utils.compat.pairs"
local argparse = require "argparse"
local fixes = require "wrap.fixes"
local getKeys = require "common.utils.table.getKeys"
local getValueKey = require "common.utils.table.getValueKey"
local makeAutoconf = require "wrap.makeAutoconf"
local makeConfig = require "wrap.makeConfig"
local map = require "common.utils.array.map"
local autoconfToYaml = require "du.script-config.autoconfToYaml"
local configToJson = require "du.script-config.configToJson"
local templates = require "wrap.templates"
local format = string.format
local concat, sort = table.concat, table.sort
local function listSlotTypes ()
local slotTypes = require "wrap.slotTypes"
local slotTypeNames = getKeys(slotTypes)
sort(slotTypeNames)
print("Slot types:")
for _, slotTypeName in ipairs(slotTypeNames) do
local filterSignatures = slotTypes[slotTypeName].filters or {}
if #filterSignatures > 0 then
local quotedFilterSignatures = map(filterSignatures, function (filterSignature) return format("%q", filterSignature) end)
print(format("* Slot type %q adds event handlers for %s.", slotTypeName, concat(quotedFilterSignatures, ", ")))
else
print(format("* Slot type %q adds no event handlers.", slotTypeName))
end
end
os.exit(0)
end
local function checkOutputType (value)
if not value then
return "json"
end
if value ~= "json" and value ~= "yaml" then
error(format('Invalid output type "%s". Must be "json" or "yaml".'), value)
end
return value
end
local function parseSlotOptions (options, slot)
slot = slot or {}
local validKeys = { "class", "select", "type" }
local function isKeyValid (key) return getValueKey(validKeys, key) end
local function addToSlot (key, value)
if not isKeyValid(key) then
error(format('Invalid key "%s" in slot options.', key))
end
slot[key] = value
end
repeat
local prevOptions = options
options = options:gsub(",([^=,]+)=([^=]*)$", function (...)
addToSlot(...)
return ""
end)
until prevOptions == options
local lastKey, lastValue = options:match("^([^=]+)=(.*)$")
if not lastKey then
error('Slot options must look like "key1=value1,key2=value".')
else
addToSlot(lastKey, lastValue)
end
return slot
end
local function parseSlot (slotDef)
slotDef = tostring(slotDef)
local name, options = slotDef:match("^([^:]+):?(.*)$")
if not name:match("^[_%a][_%w]*$") then
error(format('Invalid slot name "%s".', name))
end
local slot = { name = name }
if #options > 0 then
parseSlotOptions(options, slot)
end
return slot
end
local function getArgs ()
local parser = argparse(arg[0], [[
This script generates a DU-compatible script configuration from a single .lua file.
To use this script, structure your code like this:
script = {}
function script.onActionStart (actionName)
-- handle the actionStart event:
screen.setCenteredText("action start: " .. actionName)
end
function script.onActionStop (actionName)
-- handle the actionStop event:
screen.setCenteredText("action stop: " .. actionName)
end
function script.onStop()
-- handle the unit stop event:
screen.setCenteredText("goodbye")
end
This code will be placed inside the unit start handler. All other event handlers will be generated automatically and will call methods of the script object.]])
parser:argument("input file", "The Lua file that contains script code.")
:target "inputFile"
:convert(io.open)
parser:argument("output file", "The file that the JSON configuration or the YAML autoconf will be written to. If not set, standard output will be used instead.")
:args "?"
:target "outputFile"
:convert(function (fileName) return io.open(fileName, "w") end)
parser:option("--output", [[
Output format. Valid values: "json", "yaml". Defaults to "json".
JSON output can be pasted in-game by right-clicking on the control unit.
YAML files the with .conf extension can be placed in data/lua/autoconf/custom.]])
:argname "<format>"
:convert(checkOutputType)
parser:option("--name", "Script name. Only used when outputting YAML.")
:convert(tostring)
parser:option("--slots", [[
Control unit slot names and, optionally, element types (to generate element type specific event handlers).
When output format is YAML, "class" option can be used to link elements that do not have a type defined in this script, and the "select=all" option can be used to link all elements of that type or class.
Unit, system and library slots are added automatically. Slot names default to slot1, slot2, ..., slot10.]])
:args "0-10"
:argname "<slot_name>[:type=<slot_type>[,class=<slot_class>][,select=all]]"
:convert(parseSlot)
parser:option("--methods", [[
Methods to define on the control unit. This is not very useful and is only supported when outputting JSON.]])
:args "*"
:argname "<method_signature>"
:target "methodSignatures"
parser:option("--object", "The name of the object containing the event handlers. Defaults to \"script\".")
:argname "<script_object_name>"
:convert(tostring)
parser:option("--call-with", "Event handler function call operator. Valid values: \".\", \":\". Defaults to \".\".")
:argname "<dot_or_colon>"
:target "callOperator"
parser:option("--default-slot", "Default slot options.")
:argname "type=<slot_type>"
:target "defaultSlot"
:convert(parseSlotOptions)
parser:flag("--fix-gc", [[
Adjust GC to run more frequently. This can help prevent memory overload errors.]])
:target "fixGC"
parser:flag("--fix-require", [[
Replace non-working require() with a function that looks in package.preload and package.loaded.
This is not necessary unless require() is disabled in-game again.]])
:target "fixRequire"
parser:mutex(
parser:flag("--handle-errors", "Add error handling code that displays errors on screen elements.")
:target "handleErrors",
parser:flag("--handle-errors-min", "Add minified error handling code that displays errors on screen elements.")
:target "handleErrorsMin"
)
parser:flag("--indent-json", "Indent the JSON output.")
:target "indent"
parser:flag("--list-slot-types", "Lists slot types supported by the --slots option.")
:action(listSlotTypes)
return parser:parse()
end
local args = getArgs()
local startHandlerCode = args.inputFile:read("*all")
args.inputFile:close()
local template
if args.handleErrors then
template = templates.withErrorHandling
elseif args.handleErrorsMin then
template = templates.withErrorHandlingMinified
else
template = templates.default
end
template = templates.copy(template)
if args.fixGC then
template.startEventHandler = fixes.gc .. template.startEventHandler
end
if args.fixRequire then
template.startEventHandler = fixes.require .. template.startEventHandler
end
local options = {
scriptName = args.name,
startHandlerCode = startHandlerCode,
scriptObjectName = args.object or "script",
callOperator = args.callOperator or ".",
slotDefinitions = args.slots or {},
defaultSlotDefinition = args.defaultSlot or nil,
methodSignatures = args.methodSignatures or {},
template = template
}
local wrappedCode
if args.output == "yaml" then
if args.indent then error("--indent-json flag cannot be used with YAML output.") end
local autoconf = makeAutoconf(options)
wrappedCode = autoconfToYaml(autoconf)
else
local config = makeConfig(options)
wrappedCode = configToJson(config, args.indent)
end
if args.outputFile then
args.outputFile:write(wrappedCode)
args.outputFile:close()
else
io.write(wrappedCode)
end
--------------------------------------------------------------------------------
-- wrap.lua bundle ends
--------------------------------------------------------------------------------
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment