Merge remote-tracking branch 'myk002/myk_blueprint' into develop
commit
4f976a5909
@ -1,14 +1,165 @@
|
|||||||
local _ENV = mkmodule('plugins.blueprint')
|
local _ENV = mkmodule('plugins.blueprint')
|
||||||
|
|
||||||
--[[
|
local utils = require('utils')
|
||||||
|
|
||||||
Native functions:
|
-- 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 = [=[
|
||||||
|
|
||||||
* dig(start, end, name)
|
blueprint
|
||||||
* build(start, end, name)
|
=========
|
||||||
* place(start, end, name)
|
|
||||||
* query(start, end, name)
|
|
||||||
|
|
||||||
--]]
|
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
|
||||||
|
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',
|
||||||
|
'build',
|
||||||
|
'place',
|
||||||
|
'query',
|
||||||
|
}
|
||||||
|
|
||||||
|
valid_phases = utils.invert(valid_phase_list)
|
||||||
|
|
||||||
|
local function parse_cursor(opts, arg)
|
||||||
|
local _, _, x, y, z = arg:find('^(%d+),(%d+),(%d+)$')
|
||||||
|
if not x then
|
||||||
|
qerror(('invalid argument for --cursor option: "%s"; expected format' ..
|
||||||
|
' is "<x>,<y>,<z>", for example: "30,60,150"'):format(arg))
|
||||||
|
end
|
||||||
|
-- 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
|
||||||
|
opts.start.x = tonumber(x)
|
||||||
|
opts.start.y = tonumber(y)
|
||||||
|
opts.start.z = tonumber(z)
|
||||||
|
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 to forward slashes
|
||||||
|
opts.name = name:gsub(package.config:sub(1,1), "/")
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
return utils.processArgsGetopt(args, {
|
||||||
|
{'c', 'cursor', hasArg=true,
|
||||||
|
handler=function(optarg) parse_cursor(opts, optarg) end},
|
||||||
|
{'h', 'help', handler=function() opts.help = true end},
|
||||||
|
})
|
||||||
|
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 dim or
|
||||||
|
(not negative_ok and dim < 1 or dim == 0) or
|
||||||
|
dim ~= math.floor(dim)
|
||||||
|
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
|
||||||
|
|
||||||
|
parse_positionals(opts, positionals, depth and 4 or 3)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- compatibility with old exported API. we route the request back through
|
||||||
|
-- run_command so we have a unified path for parameter processing and invariant
|
||||||
|
-- checking.
|
||||||
|
local function do_blueprint(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)
|
||||||
|
|
||||||
|
return dfhack.run_command('blueprint',
|
||||||
|
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_blueprint(s, e, n, phase) end
|
||||||
|
end
|
||||||
|
|
||||||
return _ENV
|
return _ENV
|
||||||
|
@ -0,0 +1,164 @@
|
|||||||
|
local b = require('plugins.blueprint')
|
||||||
|
|
||||||
|
-- also covers code shared between parse_gui_commandline and parse_commandline
|
||||||
|
function test.parse_gui_commandline()
|
||||||
|
local opts = {}
|
||||||
|
b.parse_gui_commandline(opts, {})
|
||||||
|
expect.table_eq({auto_phase=true, name='blueprint'}, opts)
|
||||||
|
|
||||||
|
opts = {}
|
||||||
|
b.parse_gui_commandline(opts, {'help'})
|
||||||
|
expect.table_eq({help=true}, opts)
|
||||||
|
|
||||||
|
opts = {}
|
||||||
|
b.parse_gui_commandline(opts, {'--help'})
|
||||||
|
expect.table_eq({help=true}, opts)
|
||||||
|
|
||||||
|
opts = {}
|
||||||
|
b.parse_gui_commandline(opts, {'-h'})
|
||||||
|
expect.table_eq({help=true}, opts)
|
||||||
|
|
||||||
|
opts = {}
|
||||||
|
b.parse_gui_commandline(opts, {'--cursor=1,2,3'})
|
||||||
|
expect.table_eq({auto_phase=true, name='blueprint', start={x=1,y=2,z=3}},
|
||||||
|
opts)
|
||||||
|
|
||||||
|
opts = {}
|
||||||
|
expect.error_match('invalid argument',
|
||||||
|
function() b.parse_gui_commandline(
|
||||||
|
opts, {'--cursor=-1,2,3'}) end,
|
||||||
|
'negative coordinate')
|
||||||
|
|
||||||
|
opts = {}
|
||||||
|
expect.error_match('invalid argument',
|
||||||
|
function() b.parse_gui_commandline(
|
||||||
|
opts, {'--cursor=1,b,3'}) end,
|
||||||
|
'non-numeric coordinate')
|
||||||
|
|
||||||
|
opts = {}
|
||||||
|
b.parse_gui_commandline(opts, {'imaname'})
|
||||||
|
expect.table_eq({auto_phase=true, name='imaname'}, opts)
|
||||||
|
|
||||||
|
opts = {}
|
||||||
|
expect.error_match('invalid basename',
|
||||||
|
function() b.parse_gui_commandline(opts, {''}) end)
|
||||||
|
|
||||||
|
opts = {}
|
||||||
|
b.parse_gui_commandline(opts, {'imaname', 'dig', 'query'})
|
||||||
|
expect.table_eq({auto_phase=false, name='imaname', dig=true, query=true},
|
||||||
|
opts)
|
||||||
|
|
||||||
|
opts = {}
|
||||||
|
expect.error_match('unknown phase',
|
||||||
|
function() b.parse_gui_commandline(
|
||||||
|
opts, {'imaname', 'garbagephase'}) end)
|
||||||
|
end
|
||||||
|
|
||||||
|
function test.parse_commandline()
|
||||||
|
local opts = {}
|
||||||
|
b.parse_commandline(opts, '1', '2')
|
||||||
|
expect.table_eq({auto_phase=true,name='blueprint',width=1,height=2,depth=1},
|
||||||
|
opts)
|
||||||
|
|
||||||
|
opts = {}
|
||||||
|
b.parse_commandline(opts, '1', '2', '3')
|
||||||
|
expect.table_eq({auto_phase=true,name='blueprint',width=1,height=2,depth=3},
|
||||||
|
opts)
|
||||||
|
|
||||||
|
opts = {}
|
||||||
|
b.parse_commandline(opts, '1', '2', '-3')
|
||||||
|
expect.table_eq({auto_phase=true,name='blueprint',width=1,height=2,depth=-3},
|
||||||
|
opts)
|
||||||
|
|
||||||
|
opts = {}
|
||||||
|
b.parse_commandline(opts, '1', '2', 'imaname')
|
||||||
|
expect.table_eq({auto_phase=true,name='imaname',width=1,height=2,depth=1},
|
||||||
|
opts)
|
||||||
|
|
||||||
|
opts = {}
|
||||||
|
b.parse_commandline(opts, '1', '2', '10imaname')
|
||||||
|
expect.table_eq({auto_phase=true,name='10imaname',width=1,height=2,depth=1},
|
||||||
|
opts, 'invalid depth is considered a basename')
|
||||||
|
|
||||||
|
opts = {}
|
||||||
|
b.parse_commandline(opts, '1', '2', '-10imaname')
|
||||||
|
expect.table_eq({auto_phase=true,name='-10imaname',width=1,height=2,depth=1},
|
||||||
|
opts, 'invalid negative depth is considered a basename')
|
||||||
|
|
||||||
|
opts = {}
|
||||||
|
b.parse_commandline(opts, '1', '2', '3', 'imaname')
|
||||||
|
expect.table_eq({auto_phase=true,name='imaname',width=1,height=2,depth=3},
|
||||||
|
opts)
|
||||||
|
|
||||||
|
opts = {}
|
||||||
|
expect.error_match('invalid width or height',
|
||||||
|
function() b.parse_commandline(opts) end,
|
||||||
|
'missing width')
|
||||||
|
|
||||||
|
opts = {}
|
||||||
|
expect.error_match('invalid width or height',
|
||||||
|
function() b.parse_commandline(opts, '10') end,
|
||||||
|
'missing height')
|
||||||
|
|
||||||
|
opts = {}
|
||||||
|
expect.error_match('invalid width or height',
|
||||||
|
function() b.parse_commandline(opts, '0') end,
|
||||||
|
'zero height')
|
||||||
|
|
||||||
|
opts = {}
|
||||||
|
expect.error_match('invalid width or height',
|
||||||
|
function() b.parse_commandline(opts, 'hi') end,
|
||||||
|
'invalid width')
|
||||||
|
|
||||||
|
opts = {}
|
||||||
|
expect.error_match('invalid width or height',
|
||||||
|
function() b.parse_commandline(opts, '10', 'hi') end,
|
||||||
|
'invalid height')
|
||||||
|
|
||||||
|
opts = {}
|
||||||
|
expect.error_match('invalid depth',
|
||||||
|
function() b.parse_commandline(opts, '1', '2', '0') end,
|
||||||
|
'zero depth')
|
||||||
|
end
|
||||||
|
|
||||||
|
function test.do_blueprint_positive_dims()
|
||||||
|
local mock_run_command = mock.func()
|
||||||
|
mock.patch(dfhack, 'run_command', mock_run_command,
|
||||||
|
function()
|
||||||
|
local spos = {x=10, y=20, z=30}
|
||||||
|
local epos = {x=11, y=21, z=31}
|
||||||
|
b.query(spos, epos, 'imaname')
|
||||||
|
expect.eq(1, mock_run_command.call_count)
|
||||||
|
expect.table_eq({'blueprint', '2', '2', '2', 'imaname', 'query',
|
||||||
|
'--cursor=10,20,30'},
|
||||||
|
mock_run_command.call_args[1])
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
function test.do_blueprint_negative_dims()
|
||||||
|
local mock_run_command = mock.func()
|
||||||
|
mock.patch(dfhack, 'run_command', mock_run_command,
|
||||||
|
function()
|
||||||
|
local spos = {x=11, y=21, z=31}
|
||||||
|
local epos = {x=10, y=20, z=30}
|
||||||
|
b.query(spos, epos, 'imaname')
|
||||||
|
expect.eq(1, mock_run_command.call_count)
|
||||||
|
expect.table_eq({'blueprint', '2', '2', '-2', 'imaname', 'query',
|
||||||
|
'--cursor=10,20,31'},
|
||||||
|
mock_run_command.call_args[1])
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
function test.do_blueprint_ensure_cursor_is_at_upper_left()
|
||||||
|
local mock_run_command = mock.func()
|
||||||
|
mock.patch(dfhack, 'run_command', mock_run_command,
|
||||||
|
function()
|
||||||
|
local spos = {x=11, y=20, z=30}
|
||||||
|
local epos = {x=10, y=21, z=31}
|
||||||
|
b.query(spos, epos, 'imaname')
|
||||||
|
expect.eq(1, mock_run_command.call_count)
|
||||||
|
expect.table_eq({'blueprint', '2', '2', '2', 'imaname', 'query',
|
||||||
|
'--cursor=10,20,30'},
|
||||||
|
mock_run_command.call_args[1])
|
||||||
|
end)
|
||||||
|
end
|
Loading…
Reference in New Issue