diff --git a/.gitignore b/.gitignore index 4962b99..1f5abc6 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ __pycache__/* cm mm save.lua cm oot save.lua +z64yaz0 diff --git a/patch/.gitignore b/patch/.gitignore index fb3bee8..2017253 100644 --- a/patch/.gitignore +++ b/patch/.gitignore @@ -2,3 +2,4 @@ patchme lips entrances.asm crc32.asm +labels.lua diff --git a/patch/argparse.lua b/patch/argparse.lua new file mode 100644 index 0000000..de44b2a --- /dev/null +++ b/patch/argparse.lua @@ -0,0 +1,1180 @@ +-- 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("") + else + add("[]") + 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 diff --git a/patch/code.asm b/patch/code.asm index 25ac5b2..cc9a2df 100644 --- a/patch/code.asm +++ b/patch/code.asm @@ -24,35 +24,14 @@ li a0, @start ; 1 (just make sure @start can be a LUI!) .org 0xC4808 ; 0x8016A2C8 -/* - ; if we've already loaded once, don't load again - lbu t0, @start ; 2 - bnez t0, + ; 1 - nop ; 1 - push 4, a0, a1, a2, ra ; 5 - li a0, @start ; 1 - li a1, @vstart ; 2 - li a2, @size ; 2 - jal @DMARomToRam ; 1 - nop ; 1 - pop 4, a0, a1, a2, ra ; 5 -+: - j @dma_hook ; 1 - nop ; 1 -; total overwriten instructions: 23 -; the original function is in setup.asm, -; and is moved into our extra file. -; we have (0x944 / 4 - 23) = 570 words of space here, should we need it. - .word 0xDEADBEEF -*/ - j @dma_hook ; 1 + j dma_hook ; 1 nop ; 1 .org 0x9F9A4 ; JR of starting_exit's function - j @load_hook ; tail call + j load_hook ; tail call .org 0x80710 - j @tunic_color_hook + j tunic_color_hook lhu t1, 0x1DB0(t1); original code .org @starting_exit diff --git a/patch/common.asm b/patch/common.asm index 7e05be9..39d16e0 100644 --- a/patch/common.asm +++ b/patch/common.asm @@ -22,11 +22,3 @@ ;[link_object_ptr]: 0x244 [scene_record_size]: 0x14 - -; TODO: use arithmetic to do this (add that to lips) -; TODO: better yet, add proper label exports to lips -; so you can write the routines anywhere -[dma_hook]: 0x807DA000 -[load_hook]: 0x807DA010 -[setup_hook]: 0x807DA020 -[tunic_color_hook]: 0x807DA030 diff --git a/patch/extra.asm b/patch/extra.asm index bcaa8e3..6e7883d 100644 --- a/patch/extra.asm +++ b/patch/extra.asm @@ -349,29 +349,3 @@ setup_hook: jpop 4, a0, ra .word 0xDEADBEEF - -.org @dma_hook - j dma_hook - nop -.word dma_hook - nop - -.org @setup_hook - j setup_hook - nop -.word setup_hook - nop - -.org @load_hook - j load_hook - nop -.word load_hook - nop - -.org @tunic_color_hook - j tunic_color_hook - nop -.word tunic_color_hook - nop - -.align 0x10 diff --git a/patch/extra.lua b/patch/extra.lua new file mode 100644 index 0000000..c0aa538 --- /dev/null +++ b/patch/extra.lua @@ -0,0 +1,62 @@ +local function strpad(num, count, pad) + num = tostring(num) + return (pad:rep(count)..num):sub(#num) +end + +local function add_zeros(num, count) + return strpad(num, count - 1, '0') +end + +local function mixed_sorter(a, b) + a = type(a) == 'number' and add_zeros(a, 16) or tostring(a) + b = type(b) == 'number' and add_zeros(b, 16) or tostring(b) + return a < b +end + +-- loosely based on http://lua-users.org/wiki/SortedIteration +-- the original didn't make use of closures for who knows why +local function order_keys(t) + local oi = {} + for key in pairs(t) do + table.insert(oi, key) + end + table.sort(oi, mixed_sorter) + return oi +end + +local function opairs(t, cache) + local oi = cache and cache[t] or order_keys(t) + if cache then + cache[t] = oi + end + local i = 0 + return function() + i = i + 1 + local key = oi[i] + if key then return key, t[key] end + end +end + +local function traverse(path) + if not path then return end + local parent = _G + local key + for w in path:gfind("[%w_]+") do + if key then + parent = rawget(parent, key) + if type(parent) ~= 'table' then return end + end + key = w + end + if not key then return end + return {parent=parent, key=key} +end + +return { + strpad = strpad, + add_zeros = add_zeros, + mixed_sorter = mixed_sorter, + order_keys = order_keys, + opairs = opairs, + traverse = traverse, +} diff --git a/patch/mm-bq b/patch/mm-bq index 27b4994..1eeeae7 100755 --- a/patch/mm-bq +++ b/patch/mm-bq @@ -37,6 +37,8 @@ comp() { rm patchme/"$1" } +[ ! -s ../z64yaz0 ] && cc -O3 ../z64yaz0.c -o ../z64yaz0 + if [ $fast -eq 0 ] || [ ! -d patchme ]; then [ -d patchme ] && rm -r patchme (cd ..; ./z64dump.py -c "$rom") @@ -50,11 +52,12 @@ mkdir -p lips cp "$lips"/* lips cp "$inject/"{crc32,entrances}.asm . -touch "extra" +dd if=/dev/zero of=extra bs=370688 count=1 2>/dev/null -luajit patch.lua code.asm patchme/"$code" 0 -luajit patch.lua extra.asm extra 0x80780000 +luajit patch.lua -e labels.lua -o 0x80780000 extra.asm extra +luajit patch.lua -i labels.lua code.asm patchme/"$code" +# ensure the file is the proper size (Lua seems to expand it?) dd if=extra of=patchme/"$extra" bs=370688 count=1 2>/dev/null rm extra diff --git a/patch/oot-widescreen b/patch/oot-widescreen old mode 100644 new mode 100755 index 73e81a1..45c2631 --- a/patch/oot-widescreen +++ b/patch/oot-widescreen @@ -10,6 +10,6 @@ lips=../Lua/lib/lips mkdir -p lips cp "$lips"/* lips -luajit patch.lua widescreen-inline.asm oot-widescreen.z64 0 0x035D0000 0x80700000 +luajit patch.lua -o 0 --extra-rom 0x035D0000 --extra-ram 0x80700000 widescreen-inline.asm oot-widescreen.z64 (cd ..; ./z64dump.py -f patch/oot-widescreen.z64) diff --git a/patch/patch.lua b/patch/patch.lua index f81bf4b..490f35a 100644 --- a/patch/patch.lua +++ b/patch/patch.lua @@ -1,22 +1,32 @@ package.path = package.path..";./?/init.lua" local assemble = require "lips" +local cereal = require "serialize" +local argparse = require "argparse" -local function inject(patch, target, offset, erom, eram) - offset = offset or 0 +local function inject(args) + args.offset = args.offset or 0 - local f = io.open(target, 'r+b') + local f = io.open(args.output, 'r+b') if not f then - print("file not found:", target) + print("file not found:", args.output) return end + local state = {} + for _, import in ipairs(args.import) do + local new_state = cereal.deserialize(import) + for k, v in pairs(new_state) do + state[k] = v + end + end + local function write(pos, line) assert(#line == 2, "that ain't const") - if erom and eram and pos >= eram then - pos = pos - eram + erom - elseif pos >= offset then - pos = pos - offset + if args.extra_rom and args.extra_ram and pos >= args.extra_ram then + pos = pos - args.extra_ram + args.extra_rom + elseif pos >= args.offset then + pos = pos - args.offset end if pos >= 1024*1024*1024 then print("you probably don't want to do this:") @@ -26,30 +36,41 @@ local function inject(patch, target, offset, erom, eram) f:seek('set', pos) -- TODO: write hex dump format of written bytes - print(("%08X %s"):format(pos, line)) + --print(("%08X %s"):format(pos, line)) f:write(string.char(tonumber(line, 16))) end - -- offset assembly labels so they work properly, and assemble! - assemble(patch, write, {unsafe=true, offset=offset}) + assemble(args.input, write, {unsafe=true, offset=args.offset, labels=state}) + + if args.export then + cereal.serialize(args.export, state) + end f:close() end local function parsenum(s) - if s:sub(2) == '0x' then - s = tonumber(s, 16) + if s:sub(1, 2) == '0x' then + return tonumber(s, 16) + elseif s:sub(1, 1) == '0' then + return tonumber(s, 8) else - s = tonumber(s) + return tonumber(s) end - return s end -local offset = arg[3] -local extra_rom = arg[4] -local extra_ram = arg[5] -if offset then offset = parsenum(offset) end -if extra_rom then extra_rom = parsenum(extra_rom) end -if extra_ram then extra_ram = parsenum(extra_ram) end -inject(arg[1], arg[2], offset, extra_rom, extra_ram) +local ap = argparse("patch", "patch a binary file with assembly") + +ap:argument("input", "input assembly file") +ap:argument("output", "output binary file") +ap:option("-o --offset", "offset to pass to lips", "0"):convert(parsenum) +ap:option("-i --import", "import state file(s) containing labels"):count("*") +ap:option("-e --export", "export state file containing labels") +--ap:option("-s --state", "--import and --export to this file") +ap:option("--extra-rom", "dumb stuff"):convert(parsenum) +ap:option("--extra-ram", "dumb stuff"):convert(parsenum) + +local inject_args = ap:parse() + +inject(inject_args) diff --git a/patch/serialize.lua b/patch/serialize.lua new file mode 100644 index 0000000..44f1726 --- /dev/null +++ b/patch/serialize.lua @@ -0,0 +1,76 @@ +-- it's simple, dumb, unsafe, incomplete, and it gets the damn job done + +local type = type +local extra = require "extra" +local opairs = extra.opairs +local tostring = tostring +local open = io.open +local strfmt = string.format +local strrep = string.rep + +local function kill_bom(s) + if #s >= 3 and s:byte(1)==0xEF and s:byte(2)==0xBB and s:byte(3)==0xBF then + return s:sub(4) + end + return s +end + +local function sanitize(v) + local force = type(v) == 'string' and v:sub(1, 1):match('%d') + force = force and true or false + return type(v) == 'string' and strfmt('%q', v) or tostring(v), force +end + +local function _serialize(value, writer, level) + level = level or 1 + if type(value) == 'table' then + local indent = strrep('\t', level) + writer('{\n') + for key,value in opairs(value) do + local sane, force = sanitize(key) + local keyval = (sane == '"'..key..'"' and not force) and key or '['..sane..']' + writer(indent..keyval..' = ') + _serialize(value, writer, level + 1) + writer(',\n') + end + writer(strrep('\t', level - 1)..'}') + else + local sane, force = sanitize(value) + writer(sane) + end +end + +local function _deserialize(script) + local f = loadstring(kill_bom(script)) + if f ~= nil then + return f() + else + print('WARNING: no function to deserialize with') + return nil + end +end + +local function serialize(path, value) + local file = open(path, 'w') + if not file then return end + file:write("return ") + _serialize(value, function(...) + file:write(...) + end) + file:write("\n") + file:close() +end + +local function deserialize(path) + local file = open(path, 'r') + if not file then return end + local script = file:read('*a') + local value = _deserialize(script) + file:close() + return value +end + +return { + serialize = serialize, + deserialize = deserialize, +}