Skip to content

Commit 4be7fea

Browse files
committed
Merge remote-tracking branch 'myk002/myk_gui_blueprint'
2 parents 35d2baf + c7ab673 commit 4be7fea

File tree

3 files changed

+263
-2
lines changed

3 files changed

+263
-2
lines changed

changelog.txt

+2-1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ that repo.
1616
## New Scripts
1717
- `devel/block-borders`: overlay that displays map block borders
1818
- `devel/luacov`: generate code test coverage reports for script development. define the ``DFHACK_ENABLE_LUACOV=1`` environment variable to start gathering coverage metrics.
19+
- `gui/blueprint`: interactive frontend for the `blueprint` plugin
1920
- `gui/mass-remove`: mass removal/suspension tool for buildings and constructions
2021
- `reveal-hidden-sites`: exposes all undiscovered sites
2122

@@ -30,7 +31,7 @@ that repo.
3031
- `quickfort`: improved handling of non-rectangular and non-solid extent-based structures (like fancy-shaped stockpiles and farm plots)
3132
- `quickfort`: fixed conversion of numbers to DF keycodes in ``#query`` blueprints
3233
- `quickfort`: fixed various errors with cropping across the map edge
33-
- `quickfort`: properly reset config to default values in ``quickfort reset`` even if if the ``dfhack-config/quickfort/quickfort.txt`` config file doesn't mention all config vars. aso now works even if the config file doesn't exist
34+
- `quickfort`: properly reset config to default values in ``quickfort reset`` even if if the ``dfhack-config/quickfort/quickfort.txt`` config file doesn't mention all config vars. also now works even if the config file doesn't exist.
3435

3536
## Misc Improvements
3637
- `devel/annc-monitor`: added ``report enable|disable`` subcommand to filter combat reports

gui/blueprint.lua

+156-1
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,159 @@ values set in the interface. See the `blueprint` documentation for information
1919
on the possible parameters and options.
2020
]====]
2121

22-
print('coming soon!')
22+
local gui = require('gui')
23+
local guidm = require('gui.dwarfmode')
24+
local widgets = require('gui.widgets')
25+
26+
local function get_dims(pos1, pos2)
27+
local width, height, depth = math.abs(pos1.x - pos2.x) + 1,
28+
math.abs(pos1.y - pos2.y) + 1,
29+
math.abs(pos1.z - pos2.z) + 1
30+
return width, height, depth
31+
end
32+
33+
ActionPanel = defclass(ActionPanel, widgets.Panel)
34+
ActionPanel.ATTRS{
35+
get_mark_fn=DEFAULT_NIL,
36+
}
37+
function ActionPanel:init()
38+
self:addviews{
39+
widgets.Label{
40+
text={{text=self:callback('get_action_text')}},
41+
frame={t=0},
42+
},
43+
widgets.Label{
44+
text='with the cursor.',
45+
frame={t=1},
46+
},
47+
}
48+
end
49+
function ActionPanel:get_action_text()
50+
if self.get_mark_fn() then
51+
return 'Select the second corner'
52+
end
53+
return 'Select the first corner'
54+
end
55+
56+
BlueprintUI = defclass(BlueprintUI, guidm.MenuOverlay)
57+
BlueprintUI.ATTRS {
58+
presets={},
59+
frame_inset=1,
60+
focus_path='blueprint',
61+
}
62+
function BlueprintUI:init()
63+
local summary = {
64+
'Create quickfort blueprints\n',
65+
'from a live game map.'
66+
}
67+
68+
self:addviews{
69+
widgets.Label{text='Blueprint', frame={t=0}},
70+
widgets.Label{text=summary, text_pen=COLOR_GREY, frame={t=2}},
71+
ActionPanel{get_mark_fn=function() return self.mark end, frame={t=5}},
72+
widgets.Label{text={{text=function() return self:get_cancel_label() end,
73+
key='LEAVESCREEN', key_sep=': ',
74+
on_activate=function() self:on_cancel() end}},
75+
frame={t=8}},
76+
}
77+
end
78+
79+
function BlueprintUI:onAboutToShow()
80+
if not dfhack.isMapLoaded() then
81+
qerror('Please load a fortress map.')
82+
end
83+
end
84+
85+
function BlueprintUI:on_mark(pos)
86+
self.mark = pos
87+
end
88+
89+
function BlueprintUI:get_cancel_label()
90+
if self.mark then
91+
return 'Cancel selection'
92+
end
93+
return 'Back'
94+
end
95+
96+
function BlueprintUI:on_cancel()
97+
if self.mark then
98+
self.mark = nil
99+
else
100+
self:dismiss()
101+
end
102+
end
103+
104+
-- Sorts and returns the given arguments.
105+
local function min_to_max(...)
106+
local args = {...}
107+
table.sort(args, function(a, b) return a < b end)
108+
return table.unpack(args)
109+
end
110+
111+
local fg, bg = COLOR_GREEN, COLOR_BLACK
112+
113+
function BlueprintUI:onRenderBody()
114+
if not self.mark then return end
115+
116+
local vp = self:getViewport()
117+
local dc = gui.Painter.new(self.df_layout.map)
118+
119+
if gui.blink_visible(500) then
120+
local cursor = df.global.cursor
121+
-- clip blinking region to viewport
122+
local _,y_start,y_end = min_to_max(self.mark.y, cursor.y, vp.y1, vp.y2)
123+
local _,x_start,x_end = min_to_max(self.mark.x, cursor.x, vp.x1, vp.x2)
124+
for y=y_start,y_end do for x=x_start,x_end do
125+
local pos = xyz2pos(x, y, cursor.z)
126+
-- don't overwrite the cursor so the user can still tell where it is
127+
if not same_xyz(cursor, pos) then
128+
local stile = vp:tileToScreen(pos)
129+
dc:map(true):seek(stile.x, stile.y):
130+
pen(fg, bg):char('X'):map(false)
131+
end
132+
end end
133+
end
134+
end
135+
136+
function BlueprintUI:onInput(keys)
137+
if self:inputToSubviews(keys) then return true end
138+
139+
if keys.SELECT then
140+
local pos = guidm.getCursorPos()
141+
if self.mark then
142+
self:commit(pos)
143+
self:dismiss()
144+
else
145+
self:on_mark(pos)
146+
end
147+
return true
148+
end
149+
150+
return self:propagateMoveKeys(keys)
151+
end
152+
153+
-- assemble and execute the blueprint commandline
154+
function BlueprintUI:commit(pos)
155+
local mark = self.mark
156+
local width, height, depth = get_dims(mark, pos)
157+
if depth > 1 then
158+
-- when there are multiple levels, process them top to bottom
159+
depth = -depth
160+
end
161+
local basename = "blueprint"
162+
local cmd = {'blueprint',
163+
tostring(width), tostring(height), tostring(depth),
164+
basename}
165+
166+
-- set cursor to top left corner of the *uppermost* z-level
167+
local x, y, z = math.min(mark.x, pos.x), math.min(mark.y, pos.y),
168+
math.max(mark.z, pos.z)
169+
table.insert(cmd, ('--cursor=%d,%d,%d'):format(x, y, z))
170+
171+
print('running: ' .. table.concat(cmd, ' '))
172+
dfhack.run_command(cmd)
173+
end
174+
175+
if not dfhack_flags.module then
176+
BlueprintUI{}:show()
177+
end

test/gui/blueprint.lua

+105
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
config = {
2+
mode = 'fortress',
3+
}
4+
5+
local b = reqscript('gui/blueprint')
6+
local gui = require('gui')
7+
local guidm = require('gui.dwarfmode')
8+
9+
function test.fail_if_no_map_loaded()
10+
local mock_is_map_loaded = mock.func(false)
11+
mock.patch(dfhack, 'isMapLoaded', mock_is_map_loaded,
12+
function()
13+
expect.error_match('load a fortress map',
14+
function() b.BlueprintUI{}:show() end)
15+
end)
16+
end
17+
18+
local function send_keys(...)
19+
local keys = {...}
20+
for _,key in ipairs(keys) do
21+
gui.simulateInput(dfhack.gui.getCurViewscreen(true), key)
22+
end
23+
delay()
24+
end
25+
26+
local function load_ui()
27+
-- remove screen mode switching once gui/blueprint supports it natively
28+
for i=1,10 do
29+
if df.global.ui.main.mode == df.ui_sidebar_mode.Default and
30+
'dwarfmode/Default' == dfhack.gui.getCurFocus(true) then
31+
send_keys('D_LOOK')
32+
b.BlueprintUI{}:show()
33+
delay()
34+
return
35+
end
36+
send_keys('LEAVESCREEN')
37+
end
38+
error('Unable to get into look mode from current UI viewscreen.')
39+
end
40+
41+
function test.minimal_happy_path()
42+
local mock_print, mock_run_command = mock.func(), mock.func()
43+
mock.patch({
44+
{b, 'print', mock_print},
45+
{dfhack, 'run_command', mock_run_command},
46+
{gui, 'blink_visible', mock.func(true)},
47+
},
48+
function()
49+
load_ui()
50+
expect.eq('dfhack/lua/blueprint', dfhack.gui.getCurFocus(true))
51+
guidm.setCursorPos({x=10, y=20, z=30})
52+
send_keys('SELECT')
53+
send_keys('CURSOR_RIGHT', 'CURSOR_DOWN', 'CURSOR_DOWN',
54+
'CURSOR_UP_Z', 'CURSOR_UP_Z', 'CURSOR_UP_Z')
55+
send_keys('SELECT')
56+
expect.nil_(dfhack.gui.getCurFocus(true):find('^dfhack/'))
57+
expect.eq('running: blueprint 2 3 -4 blueprint --cursor=10,20,33',
58+
mock_print.call_args[1][1])
59+
expect.table_eq({'blueprint', '2', '3', '-4', 'blueprint',
60+
'--cursor=10,20,33'},
61+
mock_run_command.call_args[1][1])
62+
end)
63+
end
64+
65+
function test.cancel_ui()
66+
local mock_print, mock_run_command = mock.func(), mock.func()
67+
mock.patch({
68+
{b, 'print', mock_print},
69+
{dfhack, 'run_command', mock_run_command},
70+
},
71+
function()
72+
load_ui()
73+
expect.eq('dfhack/lua/blueprint', dfhack.gui.getCurFocus(true))
74+
send_keys('LEAVESCREEN')
75+
expect.nil_(dfhack.gui.getCurFocus(true):find('^dfhack/'))
76+
expect.eq(0, mock_print.call_count)
77+
expect.eq(0, mock_run_command.call_count)
78+
end)
79+
end
80+
81+
function test.cancel_selection()
82+
local mock_print, mock_run_command = mock.func(), mock.func()
83+
mock.patch({
84+
{b, 'print', mock_print},
85+
{dfhack, 'run_command', mock_run_command},
86+
},
87+
function()
88+
load_ui()
89+
expect.eq('dfhack/lua/blueprint', dfhack.gui.getCurFocus(true))
90+
guidm.setCursorPos({x=10, y=20, z=30})
91+
send_keys('SELECT')
92+
send_keys('LEAVESCREEN')
93+
expect.eq('dfhack/lua/blueprint', dfhack.gui.getCurFocus(true))
94+
guidm.setCursorPos({x=12, y=24, z=24})
95+
send_keys('SELECT')
96+
guidm.setCursorPos({x=11, y=22, z=27})
97+
send_keys('SELECT')
98+
expect.nil_(dfhack.gui.getCurFocus(true):find('^dfhack/'))
99+
expect.eq('running: blueprint 2 3 -4 blueprint --cursor=11,22,27',
100+
mock_print.call_args[1][1])
101+
expect.table_eq({'blueprint', '2', '3', '-4', 'blueprint',
102+
'--cursor=11,22,27'},
103+
mock_run_command.call_args[1][1])
104+
end)
105+
end

0 commit comments

Comments
 (0)