2018-05-08 12:42:41 -06:00
|
|
|
local _ENV = mkmodule('plugins.blueprint')
|
|
|
|
|
2021-07-03 00:30:59 -06:00
|
|
|
local argparse = require('argparse')
|
2021-05-04 14:19:49 -06:00
|
|
|
local utils = require('utils')
|
2018-05-08 12:42:41 -06:00
|
|
|
|
2021-05-04 14:19:49 -06:00
|
|
|
-- 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.
|
2021-05-07 15:07:37 -06:00
|
|
|
local help_text = [=[
|
2018-05-08 12:42:41 -06:00
|
|
|
|
2021-05-04 14:19:49 -06:00
|
|
|
blueprint
|
|
|
|
=========
|
2018-05-08 12:42:41 -06:00
|
|
|
|
2021-05-04 14:19:49 -06:00
|
|
|
Records the structure of a portion of your fortress in quickfort blueprints.
|
|
|
|
|
|
|
|
Usage:
|
|
|
|
|
|
|
|
blueprint <width> <height> [<depth>] [<name> [<phases>]] [<options>]
|
|
|
|
blueprint gui [<name> [<phases>]] [<options>]
|
|
|
|
|
|
|
|
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
|
2021-05-15 00:07:44 -06:00
|
|
|
from the active cursor on the current z-level. Output files are written to
|
|
|
|
the "blueprints" directory.
|
2021-05-04 14:19:49 -06:00
|
|
|
|
|
|
|
See the online DFHack documentation for more examples and details.
|
|
|
|
]=]
|
|
|
|
|
2021-05-07 15:07:37 -06:00
|
|
|
function print_help() print(help_text) end
|
|
|
|
|
2021-05-21 07:33:33 -06:00
|
|
|
local valid_phase_list = {
|
2021-05-04 14:19:49 -06:00
|
|
|
'dig',
|
2021-10-04 12:14:44 -06:00
|
|
|
'track',
|
2021-05-04 14:19:49 -06:00
|
|
|
'build',
|
|
|
|
'place',
|
2021-10-04 15:44:41 -06:00
|
|
|
'zone',
|
2021-05-04 14:19:49 -06:00
|
|
|
'query',
|
|
|
|
}
|
2021-05-21 07:33:33 -06:00
|
|
|
valid_phases = utils.invert(valid_phase_list)
|
|
|
|
|
2021-09-10 13:46:07 -06:00
|
|
|
local valid_formats_list = {
|
|
|
|
'minimal',
|
|
|
|
'pretty',
|
|
|
|
}
|
|
|
|
valid_formats = utils.invert(valid_formats_list)
|
|
|
|
|
2021-09-09 01:46:33 -06:00
|
|
|
local valid_split_strategies_list = {
|
|
|
|
'none',
|
|
|
|
'phase',
|
|
|
|
}
|
|
|
|
valid_split_strategies = utils.invert(valid_split_strategies_list)
|
|
|
|
|
2021-05-04 14:19:49 -06:00
|
|
|
local function parse_cursor(opts, arg)
|
2021-07-03 00:30:59 -06:00
|
|
|
local cursor = argparse.coords(arg)
|
2021-05-04 14:19:49 -06:00
|
|
|
-- 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
|
2021-07-03 00:30:59 -06:00
|
|
|
utils.assign(opts.start, cursor)
|
2021-05-04 14:19:49 -06:00
|
|
|
end
|
|
|
|
|
2021-09-10 13:46:07 -06:00
|
|
|
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, ', ')))
|
2021-09-09 01:46:33 -06:00
|
|
|
end
|
2021-09-10 13:46:07 -06:00
|
|
|
opts[name] = val
|
|
|
|
end
|
|
|
|
|
|
|
|
local function parse_format(opts, file_format)
|
|
|
|
parse_enum(opts, valid_formats, 'format', file_format)
|
2021-09-09 01:46:33 -06:00
|
|
|
end
|
|
|
|
|
2021-09-17 12:07:42 -06:00
|
|
|
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"')
|
2021-09-20 15:32:25 -06:00
|
|
|
:format(x_str or '', y_str or ''))
|
2021-09-17 12:07:42 -06:00
|
|
|
end
|
|
|
|
|
|
|
|
if not opts.playback_start then opts.playback_start = {} end
|
2021-09-20 15:32:25 -06:00
|
|
|
opts.playback_start.x, opts.playback_start.y = x, y
|
|
|
|
if #arg_list > 0 then
|
|
|
|
opts.playback_start_comment = table.concat(arg_list, ', ')
|
|
|
|
end
|
2021-09-17 12:07:42 -06:00
|
|
|
end
|
|
|
|
|
|
|
|
local function parse_split_strategy(opts, strategy)
|
|
|
|
parse_enum(opts, valid_split_strategies, 'split_strategy', strategy)
|
|
|
|
end
|
|
|
|
|
2021-05-04 14:19:49 -06:00
|
|
|
local function parse_positionals(opts, args, start_argidx)
|
2021-05-07 15:07:37 -06:00
|
|
|
local argidx = start_argidx or 1
|
2021-05-04 14:19:49 -06:00
|
|
|
|
|
|
|
-- 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
|
2021-06-29 15:25:30 -06:00
|
|
|
-- normalize paths and remove leading slashes
|
|
|
|
opts.name = utils.normalizePath(name):gsub('^/', '')
|
2021-05-04 14:19:49 -06:00
|
|
|
|
|
|
|
local auto_phase = true
|
|
|
|
local phase = args[argidx]
|
|
|
|
while phase do
|
2021-05-21 07:33:33 -06:00
|
|
|
if not valid_phases[phase] then
|
2021-09-09 01:46:33 -06:00
|
|
|
qerror(('unknown phase: "%s"; expected one of: %s')
|
|
|
|
:format(phase, table.concat(valid_phase_list, ', ')))
|
2021-05-04 14:19:49 -06:00
|
|
|
end
|
2021-05-21 07:33:33 -06:00
|
|
|
auto_phase = false
|
|
|
|
opts[phase] = true
|
2021-05-04 14:19:49 -06:00
|
|
|
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
|
|
|
|
|
2021-09-10 13:46:07 -06:00
|
|
|
-- set defaults
|
|
|
|
opts.format = valid_formats_list[1]
|
|
|
|
opts.split_strategy = valid_split_strategies_list[1]
|
|
|
|
|
2021-09-09 01:46:33 -06:00
|
|
|
local positionals = argparse.processArgsGetopt(args, {
|
2021-05-04 14:19:49 -06:00
|
|
|
{'c', 'cursor', hasArg=true,
|
|
|
|
handler=function(optarg) parse_cursor(opts, optarg) end},
|
2021-09-10 13:46:07 -06:00
|
|
|
{'f', 'format', hasArg=true,
|
|
|
|
handler=function(optarg) parse_format(opts, optarg) end},
|
2021-05-04 14:19:49 -06:00
|
|
|
{'h', 'help', handler=function() opts.help = true end},
|
2021-09-17 12:07:42 -06:00
|
|
|
{'s', 'playback-start', hasArg=true,
|
|
|
|
handler=function(optarg) parse_start(opts, optarg) end},
|
2021-09-09 01:46:33 -06:00
|
|
|
{'t', 'splitby', hasArg=true,
|
|
|
|
handler=function(optarg) parse_split_strategy(opts, optarg) end},
|
2021-05-04 14:19:49 -06:00
|
|
|
})
|
2021-09-09 01:46:33 -06:00
|
|
|
|
|
|
|
if opts.help then
|
|
|
|
return
|
|
|
|
end
|
|
|
|
|
|
|
|
return positionals
|
2021-05-04 14:19:49 -06:00
|
|
|
end
|
|
|
|
|
2021-05-05 14:22:40 -06:00
|
|
|
-- used by the gui/blueprint script
|
2021-05-04 14:19:49 -06:00
|
|
|
function parse_gui_commandline(opts, args)
|
|
|
|
local positionals = process_args(opts, args)
|
|
|
|
if opts.help then return end
|
2021-05-07 15:07:37 -06:00
|
|
|
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)
|
2021-09-17 12:07:42 -06:00
|
|
|
return not is_int(dim) or
|
|
|
|
(not negative_ok and dim < 1 or dim == 0)
|
2021-05-04 14:19:49 -06:00
|
|
|
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
|
2021-05-07 15:07:37 -06:00
|
|
|
qerror(('invalid width or height: "%s" "%s"; width and height must' ..
|
2021-05-04 14:19:49 -06:00
|
|
|
' 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
|
|
|
|
|
2021-09-17 12:07:42 -06:00
|
|
|
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
|
|
|
|
|
2021-05-04 14:19:49 -06:00
|
|
|
parse_positionals(opts, positionals, depth and 4 or 3)
|
|
|
|
end
|
|
|
|
|
2021-09-09 01:46:33 -06:00
|
|
|
-- 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
|
|
|
|
|
2021-05-21 07:52:16 -06:00
|
|
|
-- compatibility with old exported API.
|
|
|
|
local function do_phase(start_pos, end_pos, name, phase)
|
2021-05-04 14:19:49 -06:00
|
|
|
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)
|
|
|
|
|
2021-05-21 07:52:16 -06:00
|
|
|
run(tostring(width), tostring(height), tostring(depth), tostring(name),
|
|
|
|
phase, cursor)
|
2021-05-04 14:19:49 -06:00
|
|
|
end
|
|
|
|
for phase in pairs(valid_phases) do
|
2021-05-21 07:52:16 -06:00
|
|
|
_ENV[phase] = function(s, e, n) do_phase(s, e, n, phase) end
|
2021-05-04 14:19:49 -06:00
|
|
|
end
|
2018-05-08 12:42:41 -06:00
|
|
|
|
|
|
|
return _ENV
|