local _ENV = mkmodule('plugins.blueprint') local argparse = require('argparse') local utils = require('utils') -- the info here is very basic and minimal, so hopefully we won't need to change -- it when features are added and the full blueprint docs in Plugins.rst are -- updated. local help_text = [=[ blueprint ========= Records the structure of a portion of your fortress in quickfort blueprints. Usage: blueprint [] [ []] [] blueprint gui [ []] [] Examples: blueprint gui Runs gui/blueprint, the interactive blueprint frontend, where all configuration can be set visually and interactively. blueprint 30 40 bedrooms Generates blueprints for an area 30 tiles wide by 40 tiles tall, starting from the active cursor on the current z-level. Output files are written to the "blueprints" directory. See the online DFHack documentation for more examples and details. ]=] function print_help() print(help_text) end local valid_phase_list = { 'dig', 'track', 'build', 'place', 'zone', 'query', } valid_phases = utils.invert(valid_phase_list) local valid_formats_list = { 'minimal', 'pretty', } valid_formats = utils.invert(valid_formats_list) local valid_split_strategies_list = { 'none', 'phase', } valid_split_strategies = utils.invert(valid_split_strategies_list) local function parse_cursor(opts, arg) local cursor = argparse.coords(arg) -- be careful not to replace struct members when called from C++, but also -- create the table as needed when called from lua if not opts.start then opts.start = {} end utils.assign(opts.start, cursor) end local function parse_enum(opts, valid, name, val) if not valid[val] then qerror(('unknown %s: "%s"; expected one of: %s') :format(name, val, table.concat(valid, ', '))) end opts[name] = val end local function parse_format(opts, file_format) parse_enum(opts, valid_formats, 'format', file_format) end local function is_int(val) return val and val == math.floor(val) end local function is_positive_int(val) return is_int(val) and val > 0 end local function parse_start(opts, args) local arg_list = argparse.stringList(args) local x_str, y_str = table.remove(arg_list, 1), table.remove(arg_list, 1) local x, y = tonumber(x_str), tonumber(y_str) if not is_positive_int(x) or not is_positive_int(y) then qerror(('playback start offsets must be positive integers: "%s", "%s"') :format(x_str or '', y_str or '')) end if not opts.playback_start then opts.playback_start = {} end opts.playback_start.x, opts.playback_start.y = x, y if #arg_list > 0 then opts.playback_start_comment = table.concat(arg_list, ', ') end end local function parse_split_strategy(opts, strategy) parse_enum(opts, valid_split_strategies, 'split_strategy', strategy) end local function parse_positionals(opts, args, start_argidx) local argidx = start_argidx or 1 -- set defaults opts.name, opts.auto_phase = 'blueprint', true local name = args[argidx] if not name then return end if name == '' then qerror(('invalid basename: "%s"; must be a valid, non-empty pathname') :format(args[argidx])) end argidx = argidx + 1 -- normalize paths and remove leading slashes opts.name = utils.normalizePath(name):gsub('^/', '') local auto_phase = true local phase = args[argidx] while phase do if not valid_phases[phase] then qerror(('unknown phase: "%s"; expected one of: %s') :format(phase, table.concat(valid_phase_list, ', '))) end auto_phase = false opts[phase] = true argidx = argidx + 1 phase = args[argidx] end opts.auto_phase = auto_phase end local function process_args(opts, args) if args[1] == 'help' then opts.help = true return end -- set defaults opts.format = valid_formats_list[1] opts.split_strategy = valid_split_strategies_list[1] local positionals = argparse.processArgsGetopt(args, { {'c', 'cursor', hasArg=true, handler=function(optarg) parse_cursor(opts, optarg) end}, {'f', 'format', hasArg=true, handler=function(optarg) parse_format(opts, optarg) end}, {'h', 'help', handler=function() opts.help = true end}, {'s', 'playback-start', hasArg=true, handler=function(optarg) parse_start(opts, optarg) end}, {'t', 'splitby', hasArg=true, handler=function(optarg) parse_split_strategy(opts, optarg) end}, }) if opts.help then return end return positionals end -- used by the gui/blueprint script function parse_gui_commandline(opts, args) local positionals = process_args(opts, args) if opts.help then return end parse_positionals(opts, positionals) end -- dimension must be a non-nil integer that is >= 1 (or at least non-zero if -- negative_ok is true) local function is_bad_dim(dim, negative_ok) return not is_int(dim) or (not negative_ok and dim < 1 or dim == 0) end function parse_commandline(opts, ...) local positionals = process_args(opts, {...}) if opts.help then return end local width, height = tonumber(positionals[1]), tonumber(positionals[2]) if is_bad_dim(width) or is_bad_dim(height) then qerror(('invalid width or height: "%s" "%s"; width and height must' .. ' be positive integers'):format(positionals[1], positionals[2])) end opts.width, opts.height, opts.depth = width, height, 1 local depth = tonumber(positionals[3]) if depth then if is_bad_dim(depth, true) then qerror(('invalid depth: "%s"; must be a non-zero integer') :format(positionals[3])) end opts.depth = depth end if opts.playback_start and opts.playback_start.x > 0 then if opts.playback_start.x > width then qerror(('playback start x offset outside width of blueprint: %d') :format(opts.playback_start.x)) end if opts.playback_start.y > height then qerror(('playback start y offset outside height of blueprint: %d') :format(opts.playback_start.y)) end end parse_positionals(opts, positionals, depth and 4 or 3) end -- returns the name of the output file for the given context function get_filename(opts, phase) local fullname = 'blueprints/' .. opts.name local _,_,basename = fullname:find('/([^/]+)/?$') if not basename then -- should not happen since opts.name should already be validated error(('could not parse basename out of "%s"'):format(fullname)) end if fullname:endswith('/') then fullname = fullname .. basename end if opts.split_strategy == 'phase' then return ('%s-%s.csv'):format(fullname, phase) end -- no splitting return ('%s.csv'):format(fullname) end -- compatibility with old exported API. local function do_phase(start_pos, end_pos, name, phase) local width = math.abs(start_pos.x - end_pos.x) + 1 local height = math.abs(start_pos.y - end_pos.y) + 1 local depth = math.abs(start_pos.z - end_pos.z) + 1 if start_pos.z > end_pos.z then depth = -depth end local x = math.min(start_pos.x, end_pos.x) local y = math.min(start_pos.y, end_pos.y) local z = start_pos.z local cursor = ('--cursor=%d,%d,%d'):format(x, y, z) run(tostring(width), tostring(height), tostring(depth), tostring(name), phase, cursor) end for phase in pairs(valid_phases) do _ENV[phase] = function(s, e, n) do_phase(s, e, n, phase) end end return _ENV