dfhack/plugins/lua/buildingplan.lua

1654 lines
56 KiB
Lua

local _ENV = mkmodule('plugins.buildingplan')
2020-08-16 00:03:49 -06:00
--[[
Native functions:
prep buildingplan for core algorithm changes Lots of refactoring and reorganizing, with only cosmetic player-visible changes. - show quickfort mode hotlkey label regardless of whether the current building type has buildingplan enabled. before, it was only shown after the user enabled buildingplan for the current building. this eliminates the extra step when enabling quickfort mode, which force-enables all building types. - changed signature of lua-exported isPlannableBuilding to take subtype and custom type in addition to building type. this is only used by quickfort, and it already sends all three params in preparation for this change - added lua-exported scheduleCycle(), which is like doCycle(), but only takes effect on the next non-paused frame. this lets quickfort run only one buildingplan cycle regardless of how many #build blueprints were run - declared a few dfhack library methods and params const so buildingplan could call them from const methods - converted buildingplan internal debug logging fn to have a printf api - reshaped buildingplan-planner API and refactored implementation in preparation for upcoming core algorithm changes for supporing all building types (no externally-visible functionality changes) - changed df::building_type params to type, subtype, custom tuple keys - introduced capability to return multiple filters per building type (though the current buildings all only have one filter per) - split monolith hook functions in buildingplan.cpp into one per scope. this significantly cleans up the code and preps the hooks to handle iterating through multiple item filters. - got rid of send_key function and replaced with better reporting of whether keys have been handled
2020-10-16 14:52:23 -06:00
* bool isPlannableBuilding(df::building_type type, int16_t subtype, int32_t custom)
2020-11-13 11:18:54 -07:00
* bool isPlannedBuilding(df::building *bld)
2020-08-16 00:03:49 -06:00
* void addPlannedBuilding(df::building *bld)
* void doCycle()
prep buildingplan for core algorithm changes Lots of refactoring and reorganizing, with only cosmetic player-visible changes. - show quickfort mode hotlkey label regardless of whether the current building type has buildingplan enabled. before, it was only shown after the user enabled buildingplan for the current building. this eliminates the extra step when enabling quickfort mode, which force-enables all building types. - changed signature of lua-exported isPlannableBuilding to take subtype and custom type in addition to building type. this is only used by quickfort, and it already sends all three params in preparation for this change - added lua-exported scheduleCycle(), which is like doCycle(), but only takes effect on the next non-paused frame. this lets quickfort run only one buildingplan cycle regardless of how many #build blueprints were run - declared a few dfhack library methods and params const so buildingplan could call them from const methods - converted buildingplan internal debug logging fn to have a printf api - reshaped buildingplan-planner API and refactored implementation in preparation for upcoming core algorithm changes for supporing all building types (no externally-visible functionality changes) - changed df::building_type params to type, subtype, custom tuple keys - introduced capability to return multiple filters per building type (though the current buildings all only have one filter per) - split monolith hook functions in buildingplan.cpp into one per scope. this significantly cleans up the code and preps the hooks to handle iterating through multiple item filters. - got rid of send_key function and replaced with better reporting of whether keys have been handled
2020-10-16 14:52:23 -06:00
* void scheduleCycle()
2020-08-16 00:03:49 -06:00
--]]
local argparse = require('argparse')
2023-02-09 01:13:53 -07:00
local gui = require('gui')
local guidm = require('gui.dwarfmode')
local overlay = require('plugins.overlay')
local utils = require('utils')
local widgets = require('gui.widgets')
require('dfhack.buildings')
2023-02-18 02:09:54 -07:00
local uibs = df.global.buildreq
local function process_args(opts, args)
if args[1] == 'help' then
opts.help = true
return
end
return argparse.processArgsGetopt(args, {
{'h', 'help', handler=function() opts.help = true end},
})
end
function parse_commandline(...)
local args, opts = {...}, {}
local positionals = process_args(opts, args)
if opts.help then
return false
end
local command = table.remove(positionals, 1)
if not command or command == 'status' then
printStatus()
elseif command == 'set' then
setSetting(positionals[1], positionals[2] == 'true')
else
return false
end
return true
end
function get_num_filters(btype, subtype, custom)
2023-02-09 01:13:53 -07:00
local filters = dfhack.buildings.getFiltersByType({}, btype, subtype, custom)
return filters and #filters or 0
end
function get_job_item(btype, subtype, custom, index)
local filters = dfhack.buildings.getFiltersByType({}, btype, subtype, custom)
if not filters or not filters[index] then return nil end
local obj = df.job_item:new()
obj:assign(filters[index])
return obj
end
2023-02-18 02:09:54 -07:00
local function get_cur_filters()
return dfhack.buildings.getFiltersByType({}, uibs.building_type,
uibs.building_subtype, uibs.custom_type)
end
local function is_choosing_area()
return uibs.selection_pos.x >= 0
end
local function get_cur_area_dims(placement_data)
if not placement_data and not is_choosing_area() then return 1, 1, 1 end
local selection_pos = placement_data and placement_data.p1 or uibs.selection_pos
local pos = placement_data and placement_data.p2 or uibs.pos
return math.abs(selection_pos.x - pos.x) + 1,
math.abs(selection_pos.y - pos.y) + 1,
math.abs(selection_pos.z - pos.z) + 1
2023-02-18 02:09:54 -07:00
end
local function get_quantity(filter, hollow, placement_data)
2023-02-18 02:09:54 -07:00
local quantity = filter.quantity or 1
local dimx, dimy, dimz = get_cur_area_dims(placement_data)
2023-02-18 02:09:54 -07:00
if quantity < 1 then
return (((dimx * dimy) // 4) + 1) * dimz
end
if hollow and dimx > 2 and dimy > 2 then
return quantity * (2*dimx + 2*dimy - 4) * dimz
2023-02-18 02:09:54 -07:00
end
return quantity * dimx * dimy * dimz
2023-02-18 02:09:54 -07:00
end
local BUTTON_START_PEN, BUTTON_END_PEN, SELECTED_ITEM_PEN = nil, nil, nil
local reset_counts_flag = false
local reset_inspector_flag = false
2023-02-16 22:17:55 -07:00
function signal_reset()
2023-02-17 00:02:34 -07:00
BUTTON_START_PEN = nil
BUTTON_END_PEN = nil
2023-02-18 02:09:54 -07:00
SELECTED_ITEM_PEN = nil
reset_counts_flag = true
reset_inspector_flag = true
end
2023-02-17 00:02:34 -07:00
local to_pen = dfhack.pen.parse
local function get_button_start_pen()
if not BUTTON_START_PEN then
local texpos_base = dfhack.textures.getControlPanelTexposStart()
BUTTON_START_PEN = to_pen{ch='[', fg=COLOR_YELLOW,
tile=texpos_base > 0 and texpos_base + 13 or nil}
end
return BUTTON_START_PEN
end
local function get_button_end_pen()
if not BUTTON_END_PEN then
local texpos_base = dfhack.textures.getControlPanelTexposStart()
BUTTON_END_PEN = to_pen{ch=']', fg=COLOR_YELLOW,
tile=texpos_base > 0 and texpos_base + 15 or nil}
end
return BUTTON_END_PEN
end
2023-02-18 02:09:54 -07:00
local function get_selected_item_pen()
if not SELECTED_ITEM_PEN then
local texpos_base = dfhack.textures.getControlPanelTexposStart()
SELECTED_ITEM_PEN = to_pen{ch='x', fg=COLOR_GREEN,
tile=texpos_base > 0 and texpos_base + 9 or nil}
end
return SELECTED_ITEM_PEN
end
2023-02-17 00:02:34 -07:00
2023-02-19 22:17:03 -07:00
BuildingplanScreen = defclass(BuildingplanScreen, gui.ZScreen)
BuildingplanScreen.ATTRS {
pass_movement_keys=true,
pass_mouse_clicks=false,
defocusable=false,
}
2023-02-09 01:13:53 -07:00
--------------------------------
2023-02-18 02:09:54 -07:00
-- ItemSelection
2023-02-09 01:13:53 -07:00
--
2023-02-19 02:58:17 -07:00
local BUILD_TEXT_PEN = to_pen{fg=COLOR_BLACK, bg=COLOR_GREEN, keep_lower=true}
local BUILD_TEXT_HPEN = to_pen{fg=COLOR_WHITE, bg=COLOR_GREEN, keep_lower=true}
2023-02-21 16:05:06 -07:00
-- map of building type -> {set=set of recently used, list=list of recently used}
-- most recent entries are at the *end* of the list
local recently_used = {}
local function sort_by_type(a, b)
local ad, bd = a.data, b.data
return ad.item_type < bd.item_type or
(ad.item_type == bd.item_type and ad.item_subtype < bd.item_subtype) or
(ad.item_type == bd.item_type and ad.item_subtype == bd.item_subtype and a.search_key < b.search_key) or
(ad.item_type == bd.item_type and ad.item_subtype == bd.item_subtype and a.search_key == b.search_key and ad.quality > bd.quality)
end
local function sort_by_recency(a, b)
local tracker = recently_used[uibs.building_type]
if not tracker then return sort_by_type(a, b) end
local recent_a, recent_b = tracker.set[a.search_key], tracker.set[b.search_key]
-- if they're both in the set, return the one with the greater index,
-- indicating more recent
if recent_a and recent_b then return recent_a > recent_b end
if recent_a and not recent_b then return true end
if not recent_a and recent_b then return false end
return sort_by_type(a, b)
end
local function sort_by_name(a, b)
return a.search_key < b.search_key or
(a.search_key == b.search_key and sort_by_type(a, b))
end
local function sort_by_quantity(a, b)
local ad, bd = a.data, b.data
return ad.quantity > bd.quantity or
(ad.quantity == bd.quantity and sort_by_type(a, b))
end
2023-02-18 02:09:54 -07:00
ItemSelection = defclass(ItemSelection, widgets.Window)
ItemSelection.ATTRS{
frame_title='Choose items',
2023-02-19 02:58:17 -07:00
frame={w=56, h=20, l=4, t=8},
2023-02-18 02:09:54 -07:00
resizable=true,
index=DEFAULT_NIL,
quantity=DEFAULT_NIL,
2023-02-19 02:58:17 -07:00
on_submit=DEFAULT_NIL,
on_cancel=DEFAULT_NIL,
2023-02-18 02:09:54 -07:00
}
function ItemSelection:init()
local filter = get_cur_filters()[self.index]
self.num_selected = 0
2023-02-19 02:58:17 -07:00
self.selected_set = {}
2023-02-19 22:17:03 -07:00
local plural = self.quantity == 1 and '' or 's'
2023-02-18 02:09:54 -07:00
self:addviews{
widgets.Label{
2023-02-19 02:58:17 -07:00
frame={t=0, l=0, r=10},
2023-02-18 02:09:54 -07:00
text={
get_desc(filter),
2023-02-19 22:17:03 -07:00
plural,
2023-02-18 02:09:54 -07:00
NEWLINE,
2023-02-19 22:17:03 -07:00
('Select up to %d item%s ('):format(self.quantity, plural),
2023-02-18 02:09:54 -07:00
{text=function() return self.num_selected end},
' selected)',
},
},
2023-02-19 02:58:17 -07:00
widgets.Label{
frame={r=0, w=9, t=0, h=3},
text_pen=BUILD_TEXT_PEN,
text_hpen=BUILD_TEXT_HPEN,
text={
' ', NEWLINE,
' Build ', NEWLINE,
' ',
},
on_click=self:callback('submit'),
},
2023-02-18 02:09:54 -07:00
widgets.FilteredList{
2023-02-19 22:17:03 -07:00
view_id='flist',
frame={t=3, l=0, r=0, b=4},
2023-02-18 02:09:54 -07:00
case_sensitive=false,
2023-02-21 16:05:06 -07:00
choices=self:get_choices(sort_by_recency),
2023-02-18 02:09:54 -07:00
icon_width=2,
2023-02-19 22:17:03 -07:00
on_submit=self:callback('toggle_group'),
},
2023-02-21 16:05:06 -07:00
widgets.CycleHotkeyLabel{
2023-02-19 22:17:03 -07:00
frame={l=0, b=2},
2023-02-21 16:05:06 -07:00
key='CUSTOM_CTRL_X',
label='Sort by:',
options={
{label='Recently used', value=sort_by_recency},
{label='Name', value=sort_by_name},
{label='Amount', value=sort_by_quantity},
},
on_change=self:callback('on_sort'),
},
widgets.HotkeyLabel{
frame={l=0, b=1},
2023-02-19 22:17:03 -07:00
key='SELECT',
2023-02-21 16:05:06 -07:00
label='Use all/none',
2023-02-19 22:17:03 -07:00
auto_width=true,
on_activate=function() self:toggle_group(self.subviews.flist.list:getSelected()) end,
},
widgets.HotkeyLabel{
2023-02-21 16:05:06 -07:00
frame={l=22, b=1},
key='CUSTOM_CTRL_D',
label='Build',
auto_width=true,
on_activate=self:callback('submit'),
},
widgets.HotkeyLabel{
frame={l=38, b=1},
2023-02-19 22:17:03 -07:00
key='LEAVESCREEN',
2023-02-21 16:05:06 -07:00
label='Go back',
2023-02-19 22:17:03 -07:00
auto_width=true,
2023-02-21 16:05:06 -07:00
on_activate=self:callback('on_cancel'),
2023-02-19 22:17:03 -07:00
},
widgets.HotkeyLabel{
2023-02-21 16:05:06 -07:00
frame={l=0, b=0},
2023-02-19 22:17:03 -07:00
key='KEYBOARD_CURSOR_RIGHT_FAST',
key_sep=' : ',
2023-02-21 16:05:06 -07:00
label='Use one',
2023-02-19 22:17:03 -07:00
auto_width=true,
on_activate=function() self:increment_group(self.subviews.flist.list:getSelected()) end,
},
widgets.Label{
2023-02-21 16:05:06 -07:00
frame={l=6, b=0, w=5},
2023-02-19 22:17:03 -07:00
text_pen=COLOR_LIGHTGREEN,
text='Right',
},
widgets.HotkeyLabel{
2023-02-21 16:05:06 -07:00
frame={l=23, b=0},
2023-02-19 22:17:03 -07:00
key='KEYBOARD_CURSOR_LEFT_FAST',
key_sep=' : ',
2023-02-21 16:05:06 -07:00
label='Use one fewer',
2023-02-19 22:17:03 -07:00
auto_width=true,
on_activate=function() self:decrement_group(self.subviews.flist.list:getSelected()) end,
},
widgets.Label{
2023-02-21 16:05:06 -07:00
frame={l=29, b=0, w=4},
2023-02-19 22:17:03 -07:00
text_pen=COLOR_LIGHTGREEN,
text='Left',
2023-02-18 02:09:54 -07:00
},
}
end
2023-02-21 16:05:06 -07:00
-- resort and restore selection
function ItemSelection:on_sort(sort_fn)
local flist = self.subviews.flist
local saved_filter = flist:getFilter()
flist:setFilter('')
flist:setChoices(self:get_choices(sort_fn), flist:getSelected())
flist:setFilter(saved_filter)
end
2023-02-18 02:09:54 -07:00
local function make_search_key(str)
local out = ''
for c in str:gmatch("[%w%s]") do
out = out .. c
end
return out
end
2023-02-21 16:05:06 -07:00
function ItemSelection:get_choices(sort_fn)
2023-02-18 02:09:54 -07:00
local item_ids = getAvailableItems(uibs.building_type,
uibs.building_subtype, uibs.custom_type, self.index - 1)
2023-02-19 22:17:03 -07:00
local buckets = {}
2023-02-18 02:09:54 -07:00
for _,item_id in ipairs(item_ids) do
local item = df.item.find(item_id)
if not item then goto continue end
local desc = dfhack.items.getDescription(item, 0, true)
if buckets[desc] then
local bucket = buckets[desc]
2023-02-19 22:17:03 -07:00
table.insert(bucket.data.item_ids, item_id)
bucket.data.quantity = bucket.data.quantity + 1
2023-02-18 02:09:54 -07:00
else
local entry = {
search_key=make_search_key(desc),
icon=self:callback('get_entry_icon', item_id),
2023-02-19 22:17:03 -07:00
data={
item_ids={item_id},
item_type=item:getType(),
item_subtype=item:getSubtype(),
quantity=1,
quality=item:getQuality(),
selected=0,
},
2023-02-18 02:09:54 -07:00
}
buckets[desc] = entry
end
::continue::
end
local choices = {}
2023-02-19 22:17:03 -07:00
for desc,choice in pairs(buckets) do
local data = choice.data
choice.text = {
{width=10, text=function() return ('[%d/%d]'):format(data.selected, data.quantity) end},
{gap=2, text=desc},
}
2023-02-18 02:09:54 -07:00
table.insert(choices, choice)
end
2023-02-21 16:05:06 -07:00
table.sort(choices, sort_fn)
2023-02-18 02:09:54 -07:00
return choices
end
2023-02-19 22:17:03 -07:00
function ItemSelection:increment_group(idx, choice)
local data = choice.data
if self.quantity <= self.num_selected then return false end
if data.selected >= data.quantity then return false end
data.selected = data.selected + 1
self.num_selected = self.num_selected + 1
local item_id = data.item_ids[data.selected]
self.selected_set[item_id] = true
return true
end
function ItemSelection:decrement_group(idx, choice)
local data = choice.data
if data.selected <= 0 then return false end
local item_id = data.item_ids[data.selected]
self.selected_set[item_id] = nil
self.num_selected = self.num_selected - 1
data.selected = data.selected - 1
return true
end
function ItemSelection:toggle_group(idx, choice)
local data = choice.data
if data.selected > 0 then
while self:decrement_group(idx, choice) do end
else
while self:increment_group(idx, choice) do end
2023-02-18 02:09:54 -07:00
end
end
function ItemSelection:get_entry_icon(item_id)
return self.selected_set[item_id] and get_selected_item_pen() or nil
end
2023-02-21 16:05:06 -07:00
local function track_recently_used(choices)
-- use same set for all subtypes
local tracker = ensure_key(recently_used, uibs.building_type)
for _,choice in ipairs(choices) do
local data = choice.data
if data.selected <= 0 then goto continue end
local key = choice.search_key
local recent_set = ensure_key(tracker, 'set')
local recent_list = ensure_key(tracker, 'list')
if recent_set[key] then
if recent_list[#recent_list] ~= key then
for i,v in ipairs(recent_list) do
if v == key then
table.remove(recent_list, i)
table.insert(recent_list, key)
break
end
end
tracker.set = utils.invert(recent_list)
end
else
-- only keep most recent 10
if #recent_list >= 10 then
-- remove least recently used from list and set
recent_set[table.remove(recent_list, 1)] = nil
end
table.insert(recent_list, key)
recent_set[key] = #recent_list
end
::continue::
end
end
2023-02-19 02:58:17 -07:00
function ItemSelection:submit()
local selected_items = {}
for item_id in pairs(self.selected_set) do
table.insert(selected_items, item_id)
end
2023-02-21 16:05:06 -07:00
if #selected_items > 0 then
track_recently_used(self.subviews.flist:getChoices())
end
2023-02-19 02:58:17 -07:00
self.on_submit(selected_items)
end
function ItemSelection:onInput(keys)
if keys.LEAVESCREEN or keys._MOUSE_R_DOWN then
self.on_cancel()
return true
2023-02-19 22:17:03 -07:00
elseif keys._MOUSE_L_DOWN then
local list = self.subviews.flist.list
local idx = list:getIdxUnderMouse()
if idx then
list:setSelected(idx)
local modstate = dfhack.internal.getModstate()
if modstate & 2 > 0 then -- ctrl
local choice = list:getChoices()[idx]
if modstate & 1 > 0 then -- shift
self:decrement_group(idx, choice)
else
self:increment_group(idx, choice)
end
return true
end
end
2023-02-19 02:58:17 -07:00
end
return ItemSelection.super.onInput(self, keys)
end
2023-02-19 01:57:30 -07:00
ItemSelectionScreen = defclass(ItemSelectionScreen, BuildingplanScreen)
ItemSelectionScreen.ATTRS {
focus_path='dwarfmode/Building/Placement/dfhack/lua/buildingplan/itemselection',
force_pause=true,
pass_pause=false,
2023-02-18 02:09:54 -07:00
index=DEFAULT_NIL,
quantity=DEFAULT_NIL,
2023-02-18 02:09:54 -07:00
on_submit=DEFAULT_NIL,
2023-02-19 02:58:17 -07:00
on_cancel=DEFAULT_NIL,
2023-02-18 02:09:54 -07:00
}
function ItemSelectionScreen:init()
self:addviews{
ItemSelection{
index=self.index,
quantity=self.quantity,
2023-02-19 02:58:17 -07:00
on_submit=self.on_submit,
on_cancel=self.on_cancel,
2023-02-18 02:09:54 -07:00
}
}
end
--------------------------------
2023-02-19 22:17:03 -07:00
-- FilterSelection
--
-- returns whether the items matched by the specified filter can have a quality
-- rating. This also conveniently indicates whether an item can be decorated.
local function can_be_improved(idx)
local filter = get_cur_filters()[idx]
if filter.flags2 and filter.flags2.building_material then
return false;
end
return filter.item_type ~= df.item_type.WOOD and
filter.item_type ~= df.item_type.BLOCKS and
filter.item_type ~= df.item_type.BAR and
filter.item_type ~= df.item_type.BOULDER
end
2023-02-23 02:15:22 -07:00
local OPTIONS_COL_WIDTH = 28
local TYPE_COL_WIDTH = 20
local HEADER_HEIGHT = 5
local FOOTER_HEIGHT = 4
2023-02-19 22:17:03 -07:00
FilterSelection = defclass(FilterSelection, widgets.Window)
FilterSelection.ATTRS{
2023-02-26 11:55:50 -07:00
frame_title='Choose filters [MOCK -- NOT FUNCTIONAL]',
2023-02-22 19:59:15 -07:00
frame={w=80, h=53, l=30, t=8},
2023-02-19 22:17:03 -07:00
resizable=true,
index=DEFAULT_NIL,
}
2023-02-23 02:15:22 -07:00
local STANDIN_PEN = to_pen{fg=COLOR_GREEN, bg=COLOR_GREEN, ch=' '}
2023-02-19 22:17:03 -07:00
function FilterSelection:init()
self:addviews{
widgets.Panel{
view_id='options_panel',
2023-02-23 02:15:22 -07:00
frame={l=0, t=0, b=FOOTER_HEIGHT, w=OPTIONS_COL_WIDTH},
autoarrange_subviews=true,
subviews={
widgets.Panel{
view_id='quality_panel',
2023-02-23 02:15:22 -07:00
frame={l=0, r=0, h=24},
frame_inset={t=1},
2023-02-22 19:59:15 -07:00
frame_style=gui.INTERIOR_FRAME,
frame_title='Item quality',
subviews={
2023-02-23 02:15:22 -07:00
widgets.HotkeyLabel{
2023-02-22 19:59:15 -07:00
frame={l=0, t=0},
2023-02-23 02:15:22 -07:00
key='CUSTOM_SHIFT_Q',
},
widgets.HotkeyLabel{
frame={l=1, t=0},
key='CUSTOM_SHIFT_W',
label='Set max quality',
2023-02-22 19:59:15 -07:00
},
widgets.Panel{
view_id='quality_slider',
frame={l=0, t=2, w=3, h=15},
2023-02-23 02:15:22 -07:00
frame_background=STANDIN_PEN,
2023-02-22 19:59:15 -07:00
},
widgets.Label{
frame={l=3, t=3},
2023-02-23 02:15:22 -07:00
text='- Artifact (1)',
2023-02-22 19:59:15 -07:00
},
widgets.Label{
frame={l=3, t=5},
2023-02-23 02:15:22 -07:00
text='- Masterful (3)',
2023-02-22 19:59:15 -07:00
},
widgets.Label{
frame={l=3, t=7},
2023-02-23 02:15:22 -07:00
text='- Exceptional (34)',
2023-02-22 19:59:15 -07:00
},
widgets.Label{
frame={l=3, t=9},
2023-02-23 02:15:22 -07:00
text='- Superior (50)',
2023-02-22 19:59:15 -07:00
},
widgets.Label{
frame={l=3, t=11},
2023-02-23 02:15:22 -07:00
text='- FinelyCrafted (67)',
2023-02-22 19:59:15 -07:00
},
widgets.Label{
frame={l=3, t=13},
2023-02-23 02:15:22 -07:00
text='- WellCrafted (79)',
2023-02-22 19:59:15 -07:00
},
widgets.Label{
frame={l=3, t=15},
2023-02-23 02:15:22 -07:00
text='- Ordinary (206)',
2023-02-22 19:59:15 -07:00
},
2023-02-23 02:15:22 -07:00
widgets.HotkeyLabel{
2023-02-22 19:59:15 -07:00
frame={l=0, t=18},
2023-02-23 02:15:22 -07:00
key='CUSTOM_SHIFT_Z',
},
widgets.HotkeyLabel{
frame={l=1, t=18},
key='CUSTOM_SHIFT_X',
label='Set min quality',
2023-02-22 19:59:15 -07:00
},
widgets.CycleHotkeyLabel{
frame={l=0, t=20},
2023-02-23 02:15:22 -07:00
key='CUSTOM_SHIFT_D',
2023-02-22 19:59:15 -07:00
label='Decorated only:',
options={'No', 'Yes'},
},
},
},
2023-02-22 19:59:15 -07:00
widgets.ResizingPanel{
view_id='building_panel',
2023-02-22 19:59:15 -07:00
frame={l=0, r=0},
2023-02-23 02:15:22 -07:00
frame_inset={t=1},
2023-02-22 19:59:15 -07:00
frame_style=gui.INTERIOR_FRAME,
frame_title='Building options',
2023-02-22 19:59:15 -07:00
autoarrange_subviews=true,
autoarrange_gap=1,
subviews={
2023-02-22 19:59:15 -07:00
widgets.WrappedLabel{
frame={l=0},
text_to_wrap='These options will affect all items for the current building type.',
},
widgets.CycleHotkeyLabel{
frame={l=0},
2023-02-23 02:15:22 -07:00
key='CUSTOM_SHIFT_G',
2023-02-22 19:59:15 -07:00
label='Building safety:',
options={
{label='Any', value=0},
{label='Magma', value=2, pen=COLOR_RED},
{label='Fire', value=1, pen=COLOR_LIGHTRED},
},
},
},
},
widgets.Panel{
view_id='global_panel',
2023-02-22 19:59:15 -07:00
frame={l=0, r=0, b=0},
2023-02-23 02:15:22 -07:00
frame_inset={t=1},
2023-02-22 19:59:15 -07:00
frame_style=gui.INTERIOR_FRAME,
frame_title='Global options',
2023-02-22 19:59:15 -07:00
autoarrange_subviews=true,
subviews={
2023-02-22 19:59:15 -07:00
widgets.WrappedLabel{
frame={l=0},
text_to_wrap='These options will affect the selection of "Generic Materials" for future buildings.',
},
2023-02-23 02:15:22 -07:00
widgets.Panel{
frame={h=1},
},
2023-02-22 19:59:15 -07:00
widgets.ToggleHotkeyLabel{
frame={l=0},
2023-02-23 02:15:22 -07:00
key='CUSTOM_SHIFT_B',
2023-02-22 19:59:15 -07:00
label='Blocks',
2023-02-23 02:15:22 -07:00
label_width=8,
2023-02-22 19:59:15 -07:00
},
widgets.ToggleHotkeyLabel{
frame={l=0},
2023-02-23 02:15:22 -07:00
key='CUSTOM_SHIFT_L',
2023-02-22 19:59:15 -07:00
label='Logs',
2023-02-23 02:15:22 -07:00
label_width=8,
2023-02-22 19:59:15 -07:00
},
widgets.ToggleHotkeyLabel{
frame={l=0},
2023-02-23 02:15:22 -07:00
key='CUSTOM_SHIFT_O',
2023-02-22 19:59:15 -07:00
label='Boulders',
2023-02-23 02:15:22 -07:00
label_width=8,
2023-02-22 19:59:15 -07:00
},
widgets.ToggleHotkeyLabel{
frame={l=0},
2023-02-23 02:15:22 -07:00
key='CUSTOM_SHIFT_P',
2023-02-22 19:59:15 -07:00
label='Bars',
2023-02-23 02:15:22 -07:00
label_width=8,
2023-02-22 19:59:15 -07:00
},
},
},
},
},
widgets.Panel{
view_id='materials_panel',
2023-02-23 02:15:22 -07:00
frame={l=OPTIONS_COL_WIDTH, t=0, b=FOOTER_HEIGHT, r=0},
subviews={
widgets.Panel{
2023-02-23 02:15:22 -07:00
view_id='header',
frame={l=0, t=0, h=HEADER_HEIGHT, r=0},
subviews={
2023-02-23 02:15:22 -07:00
widgets.EditField{
frame={l=1, t=0},
label_text='Search: ',
on_char=function(ch) return ch:match('%l') end,
},
widgets.CycleHotkeyLabel{
frame={l=1, t=2, w=21},
label='Sort by:',
key='CUSTOM_SHIFT_R',
options={'name', 'available'},
},
widgets.ToggleHotkeyLabel{
frame={l=24, t=2, w=24},
label='Hide unavailable:',
key='CUSTOM_SHIFT_H',
initial_option=false,
},
widgets.Label{
frame={l=1, b=0},
text='Type',
text_pen=COLOR_LIGHTRED,
},
widgets.Label{
frame={l=TYPE_COL_WIDTH, b=0},
text='Material',
text_pen=COLOR_LIGHTRED,
},
},
},
widgets.Panel{
view_id='materials_lists',
2023-02-23 02:15:22 -07:00
frame={l=0, t=HEADER_HEIGHT, r=0, b=0},
2023-02-22 19:59:15 -07:00
frame_style=gui.INTERIOR_FRAME,
subviews={
2023-02-23 02:15:22 -07:00
widgets.List{
view_id='materials_categories',
2023-02-23 02:15:22 -07:00
frame={l=1, t=0, b=0, w=TYPE_COL_WIDTH-3},
scroll_keys={},
choices={
{text='Stone', key='CUSTOM_SHIFT_S'},
{text='Wood', key='CUSTOM_SHIFT_W'},
{text='Metal', key='CUSTOM_SHIFT_M'},
{text='Other', key='CUSTOM_SHIFT_O'},
},
},
2023-02-23 02:15:22 -07:00
widgets.List{
view_id='materials_mats',
2023-02-23 02:15:22 -07:00
frame={l=TYPE_COL_WIDTH, t=0, r=0, b=0},
choices={
{text='9 - granite'},
{text='0 - graphite'},
},
},
},
},
2023-02-23 02:15:22 -07:00
widgets.Panel{
view_id='divider',
frame={l=TYPE_COL_WIDTH-1, t=HEADER_HEIGHT, b=0, w=1},
on_render=self:callback('draw_divider'),
}
},
},
2023-02-23 02:15:22 -07:00
widgets.Panel{
view_id='footer',
frame={l=0, r=0, b=0, h=FOOTER_HEIGHT},
frame_inset={l=20, t=1},
subviews={
widgets.HotkeyLabel{
frame={l=0, t=0},
label='Toggle',
auto_width=true,
key='SELECT',
},
widgets.HotkeyLabel{
frame={l=0, t=2},
label='Done',
auto_width=true,
key='LEAVESCREEN',
},
widgets.HotkeyLabel{
frame={l=30, t=0},
label='Select all',
auto_width=true,
key='CUSTOM_SHIFT_A',
},
widgets.HotkeyLabel{
frame={l=30, t=1},
label='Invert selection',
auto_width=true,
key='CUSTOM_SHIFT_I',
},
widgets.HotkeyLabel{
frame={l=30, t=2},
label='Clear selection',
auto_width=true,
key='CUSTOM_SHIFT_C',
},
},
}
}
2023-02-19 22:17:03 -07:00
end
2023-02-23 02:15:22 -07:00
local texpos = dfhack.textures.getThinBordersTexposStart()
local tp = function(offset)
if texpos == -1 then return nil end
return texpos + offset
end
local TOP_PEN = to_pen{tile=tp(10), ch=194, fg=COLOR_GREY, bg=COLOR_BLACK}
local MID_PEN = to_pen{tile=tp(4), ch=192, fg=COLOR_GREY, bg=COLOR_BLACK}
local BOT_PEN = to_pen{tile=tp(11), ch=179, fg=COLOR_GREY, bg=COLOR_BLACK}
function FilterSelection:draw_divider(dc)
local y2 = dc.height - 1
for y=0,y2 do
dc:seek(0, y)
if y == 0 then
dc:char(nil, TOP_PEN)
elseif y == y2 then
dc:char(nil, BOT_PEN)
else
dc:char(nil, MID_PEN)
end
end
end
2023-02-19 22:17:03 -07:00
FilterSelectionScreen = defclass(FilterSelectionScreen, BuildingplanScreen)
FilterSelectionScreen.ATTRS {
focus_path='dwarfmode/Building/Placement/dfhack/lua/buildingplan/filterselection',
2023-02-19 22:17:03 -07:00
index=DEFAULT_NIL,
}
function FilterSelectionScreen:init()
self:addviews{
FilterSelection{index=self.index}
}
end
function FilterSelectionScreen:onShow()
df.global.game.main_interface.bottom_mode_selected = -1
end
function FilterSelectionScreen:onDismiss()
df.global.game.main_interface.bottom_mode_selected = df.main_bottom_mode_type.BUILDING_PLACEMENT
end
2023-02-19 22:17:03 -07:00
--------------------------------
-- ItemLine
2023-02-18 02:09:54 -07:00
--
2023-02-09 01:13:53 -07:00
local function cur_building_has_no_area()
if uibs.building_type == df.building_type.Construction then return false end
local filters = dfhack.buildings.getFiltersByType({},
uibs.building_type, uibs.building_subtype, uibs.custom_type)
-- this works because all variable-size buildings have either no item
-- filters or a quantity of -1 for their first (and only) item
return filters and filters[1] and (not filters[1].quantity or filters[1].quantity > 0)
end
local function is_plannable()
return get_cur_filters() and
not (uibs.building_type == df.building_type.Construction
and uibs.building_subtype == df.construction_type.TrackNSEW)
end
local function is_construction()
2023-02-11 03:10:07 -07:00
return uibs.building_type == df.building_type.Construction
end
local function is_stairs()
return is_construction()
2023-02-11 03:10:07 -07:00
and uibs.building_subtype == df.construction_type.UpDownStair
end
2023-02-09 01:13:53 -07:00
local direction_panel_frame = {t=4, h=13, w=46, r=28}
local direction_panel_types = utils.invert{
df.building_type.Bridge,
df.building_type.ScrewPump,
df.building_type.WaterWheel,
df.building_type.AxleHorizontal,
df.building_type.Rollers,
}
local function has_direction_panel()
return direction_panel_types[uibs.building_type]
or (uibs.building_type == df.building_type.Trap
and uibs.building_subtype == df.trap_type.TrackStop)
end
2023-02-11 03:10:07 -07:00
local pressure_plate_panel_frame = {t=4, h=37, w=46, r=28}
local function has_pressure_plate_panel()
return uibs.building_type == df.building_type.Trap
and uibs.building_subtype == df.trap_type.PressurePlate
end
local function is_over_options_panel()
local frame = nil
if has_direction_panel() then
frame = direction_panel_frame
elseif has_pressure_plate_panel() then
frame = pressure_plate_panel_frame
else
return false
end
local v = widgets.Widget{frame=frame}
2023-02-09 01:13:53 -07:00
local rect = gui.mkdims_wh(0, 0, dfhack.screen.getWindowSize())
v:updateLayout(gui.ViewRect{rect=rect})
return v:getMousePos()
end
local function to_title_case(str)
str = str:gsub('(%a)([%w_]*)',
function (first, rest) return first:upper()..rest:lower() end)
str = str:gsub('_', ' ')
return str
end
ItemLine = defclass(ItemLine, widgets.Panel)
ItemLine.ATTRS{
idx=DEFAULT_NIL,
2023-02-17 15:24:21 -07:00
is_selected_fn=DEFAULT_NIL,
is_hollow_fn=DEFAULT_NIL,
2023-02-17 15:24:21 -07:00
on_select=DEFAULT_NIL,
on_filter=DEFAULT_NIL,
on_clear_filter=DEFAULT_NIL,
}
function ItemLine:init()
self.frame.h = 1
self.visible = function() return #get_cur_filters() >= self.idx end
self:addviews{
widgets.Label{
2023-02-17 15:24:21 -07:00
frame={t=0, l=0},
text='*',
auto_width=true,
visible=self.is_selected_fn,
},
widgets.Label{
frame={t=0, l=25},
2023-02-16 22:17:55 -07:00
text={
2023-02-17 00:02:34 -07:00
{tile=get_button_start_pen},
{gap=6, tile=get_button_end_pen},
2023-02-17 15:24:21 -07:00
},
auto_width=true,
on_click=function() self.on_filter(self.idx) end,
},
widgets.Label{
frame={t=0, l=33},
text={
2023-02-17 00:02:34 -07:00
{tile=get_button_start_pen},
{gap=1, tile=get_button_end_pen},
2023-02-16 22:17:55 -07:00
},
2023-02-17 15:24:21 -07:00
auto_width=true,
on_click=function() self.on_clear_filter(self.idx) end,
},
widgets.Label{
2023-02-17 15:24:21 -07:00
frame={t=0, l=2},
2023-02-16 22:17:55 -07:00
text={
2023-02-17 00:02:34 -07:00
{width=21, text=self:callback('get_item_line_text')},
{gap=3, text='filter', pen=COLOR_GREEN},
{gap=2, text='x', pen=self:callback('get_x_pen')},
2023-02-17 00:02:34 -07:00
{gap=3, text=function() return self.note end,
pen=function() return self.note_pen end},
2023-02-16 22:17:55 -07:00
},
},
}
end
function ItemLine:reset()
self.desc = nil
self.available = nil
end
2023-02-17 15:24:21 -07:00
function ItemLine:onInput(keys)
if keys._MOUSE_L_DOWN and self:getMousePos() then
self.on_select(self.idx)
end
return ItemLine.super.onInput(self, keys)
end
function ItemLine:get_x_pen()
return hasFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, self.idx - 1) and
2023-02-19 01:57:30 -07:00
COLOR_GREEN or COLOR_GREY
end
2023-02-15 20:10:42 -07:00
function get_desc(filter)
2023-02-09 01:13:53 -07:00
local desc = 'Unknown'
if filter.has_tool_use and filter.has_tool_use > -1 then
2023-02-09 01:13:53 -07:00
desc = to_title_case(df.tool_uses[filter.has_tool_use])
elseif filter.flags2 and filter.flags2.screw then
desc = 'Screw'
elseif filter.item_type and filter.item_type > -1 then
2023-02-09 01:13:53 -07:00
desc = to_title_case(df.item_type[filter.item_type])
elseif filter.vector_id and filter.vector_id > -1 then
desc = to_title_case(df.job_item_vector_id[filter.vector_id])
elseif filter.flags2 and filter.flags2.building_material then
desc = 'Building material';
2023-02-09 01:13:53 -07:00
if filter.flags2.fire_safe then
2023-02-15 20:10:42 -07:00
desc = 'Fire-safe material';
2023-02-09 01:13:53 -07:00
end
if filter.flags2.magma_safe then
2023-02-15 20:10:42 -07:00
desc = 'Magma-safe material';
2023-02-09 01:13:53 -07:00
end
end
2023-02-13 17:24:10 -07:00
if desc:endswith('s') then
desc = desc:sub(1,-2)
end
if desc == 'Trappart' then
desc = 'Mechanism'
elseif desc == 'Wood' then
desc = 'Log'
2023-02-13 17:24:10 -07:00
end
return desc
end
2023-02-13 17:24:10 -07:00
function ItemLine:get_item_line_text()
local idx = self.idx
local filter = get_cur_filters()[idx]
local quantity = get_quantity(filter, self.is_hollow_fn())
self.desc = self.desc or get_desc(filter)
self.available = self.available or countAvailableItems(uibs.building_type,
2023-02-13 17:24:10 -07:00
uibs.building_subtype, uibs.custom_type, idx - 1)
2023-02-17 00:02:34 -07:00
if self.available >= quantity then
self.note_pen = COLOR_GREEN
self.note = 'Available now'
else
self.note_pen = COLOR_YELLOW
self.note = 'Will link later'
end
2023-02-13 17:24:10 -07:00
2023-02-16 22:17:55 -07:00
return ('%d %s%s'):format(quantity, self.desc, quantity == 1 and '' or 's')
2023-02-09 01:13:53 -07:00
end
function ItemLine:reduce_quantity()
if not self.available then return end
local filter = get_cur_filters()[self.idx]
self.available = math.max(0, self.available - get_quantity(filter, self.is_hollow_fn()))
2023-02-09 01:13:53 -07:00
end
local function get_placement_errors()
local out = ''
for _,str in ipairs(uibs.errors) do
if #out > 0 then out = out .. NEWLINE end
out = out .. str.value
end
return out
end
2023-02-19 22:17:03 -07:00
--------------------------------
-- PlannerOverlay
--
PlannerOverlay = defclass(PlannerOverlay, overlay.OverlayWidget)
PlannerOverlay.ATTRS{
default_pos={x=5,y=9},
default_enabled=true,
viewscreens='dwarfmode/Building/Placement',
2023-02-16 19:23:14 -07:00
frame={w=56, h=20},
}
function PlannerOverlay:init()
2023-02-17 15:24:21 -07:00
self.selected = 1
local main_panel = widgets.Panel{
view_id='main',
frame={t=0, l=0, r=0, h=14},
frame_style=gui.MEDIUM_FRAME,
frame_background=gui.CLEAR_PEN,
}
2023-02-17 15:24:21 -07:00
local function make_is_selected_fn(idx)
return function() return self.selected == idx end
end
local function on_select_fn(idx)
self.selected = idx
end
local function is_hollow_fn()
return self.subviews.hollow:getOptionValue()
end
main_panel:addviews{
2023-02-09 01:13:53 -07:00
widgets.Label{
frame={},
auto_width=true,
text='No items required.',
visible=function() return #get_cur_filters() == 0 end,
},
2023-02-17 15:24:21 -07:00
ItemLine{view_id='item1', frame={t=0, l=0, r=0}, idx=1,
is_selected_fn=make_is_selected_fn(1), is_hollow_fn=is_hollow_fn,
on_select=on_select_fn, on_filter=self:callback('set_filter'),
2023-02-17 15:24:21 -07:00
on_clear_filter=self:callback('clear_filter')},
ItemLine{view_id='item2', frame={t=2, l=0, r=0}, idx=2,
is_selected_fn=make_is_selected_fn(2), is_hollow_fn=is_hollow_fn,
on_select=on_select_fn, on_filter=self:callback('set_filter'),
2023-02-17 15:24:21 -07:00
on_clear_filter=self:callback('clear_filter')},
ItemLine{view_id='item3', frame={t=4, l=0, r=0}, idx=3,
is_selected_fn=make_is_selected_fn(3), is_hollow_fn=is_hollow_fn,
on_select=on_select_fn, on_filter=self:callback('set_filter'),
2023-02-17 15:24:21 -07:00
on_clear_filter=self:callback('clear_filter')},
ItemLine{view_id='item4', frame={t=6, l=0, r=0}, idx=4,
is_selected_fn=make_is_selected_fn(4), is_hollow_fn=is_hollow_fn,
on_select=on_select_fn, on_filter=self:callback('set_filter'),
2023-02-17 15:24:21 -07:00
on_clear_filter=self:callback('clear_filter')},
widgets.CycleHotkeyLabel{
view_id='hollow',
frame={t=3, l=4},
key='CUSTOM_H',
label='Hollow area:',
visible=is_construction,
options={
{label='No', value=false},
{label='Yes', value=true},
},
},
2023-02-11 03:10:07 -07:00
widgets.CycleHotkeyLabel{
2023-02-15 20:10:42 -07:00
view_id='stairs_top_subtype',
frame={t=4, l=4},
2023-02-15 20:10:42 -07:00
key='CUSTOM_R',
2023-02-17 00:02:34 -07:00
label='Top Stair Type: ',
2023-02-11 03:10:07 -07:00
visible=is_stairs,
options={
{label='Auto', value='auto'},
{label='UpDown', value=df.construction_type.UpDownStair},
{label='Down', value=df.construction_type.DownStair},
},
},
widgets.CycleHotkeyLabel {
2023-02-15 20:10:42 -07:00
view_id='stairs_bottom_subtype',
frame={t=5, l=4},
2023-02-15 20:10:42 -07:00
key='CUSTOM_B',
label='Bottom Stair Type: ',
2023-02-11 03:10:07 -07:00
visible=is_stairs,
options={
{label='Auto', value='auto'},
{label='UpDown', value=df.construction_type.UpDownStair},
{label='Up', value=df.construction_type.UpStair},
},
},
2023-02-09 01:13:53 -07:00
widgets.Label{
frame={b=3, l=17},
2023-02-09 01:13:53 -07:00
text={
'Selected area: ',
{text=function()
return ('%dx%dx%d'):format(get_cur_area_dims(self.saved_placement))
2023-02-09 01:13:53 -07:00
end
},
},
visible=function()
return not cur_building_has_no_area() and (self.saved_placement or is_choosing_area())
end,
},
2023-02-17 15:24:21 -07:00
widgets.Panel{
visible=function() return #get_cur_filters() > 0 end,
subviews={
widgets.HotkeyLabel{
frame={b=1, l=0},
key='STRING_A042',
auto_width=true,
enabled=function() return #get_cur_filters() > 1 end,
on_activate=function() self.selected = ((self.selected - 2) % #get_cur_filters()) + 1 end,
},
widgets.HotkeyLabel{
frame={b=1, l=1},
key='STRING_A047',
label='Prev/next item',
auto_width=true,
enabled=function() return #get_cur_filters() > 1 end,
on_activate=function() self.selected = (self.selected % #get_cur_filters()) + 1 end,
2023-02-17 15:24:21 -07:00
},
widgets.HotkeyLabel{
frame={b=1, l=21},
key='CUSTOM_F',
label='Set filter',
auto_width=true,
2023-02-18 02:09:54 -07:00
on_activate=function() self:set_filter(self.selected) end,
2023-02-17 15:24:21 -07:00
},
widgets.HotkeyLabel{
frame={b=1, l=37},
2023-02-17 15:24:21 -07:00
key='CUSTOM_X',
label='Clear filter',
auto_width=true,
2023-02-17 15:24:21 -07:00
on_activate=function() self:clear_filter(self.selected) end,
enabled=function()
return hasFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, self.selected - 1)
end
2023-02-17 15:24:21 -07:00
},
widgets.CycleHotkeyLabel{
view_id='choose',
frame={b=0, l=0, w=25},
key='CUSTOM_I',
2023-02-19 01:57:30 -07:00
label='Choose from items:',
options={{label='Yes', value=true},
{label='No', value=false}},
initial_option=false,
enabled=function()
for idx = 1,4 do
if (self.subviews['item'..idx].available or 0) > 0 then
return true
end
end
end,
},
2023-02-17 15:24:21 -07:00
widgets.CycleHotkeyLabel{
view_id='safety',
frame={b=0, l=29, w=25},
2023-02-17 15:24:21 -07:00
key='CUSTOM_G',
label='Building safety:',
2023-02-17 15:24:21 -07:00
options={
2023-02-19 01:57:30 -07:00
{label='Any', value=0},
{label='Magma', value=2, pen=COLOR_RED},
{label='Fire', value=1, pen=COLOR_LIGHTRED},
2023-02-17 15:24:21 -07:00
},
2023-02-19 03:03:39 -07:00
initial_option=0,
2023-02-19 01:57:30 -07:00
on_change=function(heat)
setHeatSafetyFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, heat)
end,
2023-02-17 15:24:21 -07:00
},
2023-02-15 20:10:42 -07:00
},
},
}
local error_panel = widgets.ResizingPanel{
view_id='errors',
2023-02-16 19:23:14 -07:00
frame={t=14, l=0, r=0},
frame_style=gui.MEDIUM_FRAME,
frame_background=gui.CLEAR_PEN,
}
error_panel:addviews{
widgets.WrappedLabel{
2023-02-16 19:23:14 -07:00
frame={t=0, l=0, r=0},
text_pen=COLOR_LIGHTRED,
text_to_wrap=get_placement_errors,
2023-02-16 19:23:14 -07:00
visible=function() return #uibs.errors > 0 end,
},
widgets.Label{
2023-02-16 19:23:14 -07:00
frame={t=0, l=0, r=0},
text_pen=COLOR_GREEN,
text='OK to build',
visible=function() return #uibs.errors == 0 end,
},
}
self:addviews{
main_panel,
error_panel,
}
end
function PlannerOverlay:reset()
self.subviews.item1:reset()
self.subviews.item2:reset()
self.subviews.item3:reset()
self.subviews.item4:reset()
reset_counts_flag = false
2023-02-09 01:13:53 -07:00
end
2023-02-18 02:09:54 -07:00
function PlannerOverlay:set_filter(idx)
FilterSelectionScreen{index=idx}:show()
2023-02-17 15:24:21 -07:00
end
function PlannerOverlay:clear_filter(idx)
setMaterialFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, idx - 1, "")
2023-02-17 15:24:21 -07:00
end
local function get_placement_data()
local pos = uibs.pos
local direction = uibs.direction
local width, height, depth = get_cur_area_dims()
local _, adjusted_width, adjusted_height = dfhack.buildings.getCorrectSize(
width, height, uibs.building_type, uibs.building_subtype,
uibs.custom_type, direction)
-- get the upper-left corner of the building/area at min z-level
local has_selection = is_choosing_area()
local start_pos = xyz2pos(
has_selection and math.min(uibs.selection_pos.x, pos.x) or pos.x - adjusted_width//2,
has_selection and math.min(uibs.selection_pos.y, pos.y) or pos.y - adjusted_height//2,
has_selection and math.min(uibs.selection_pos.z, pos.z) or pos.z
)
if uibs.building_type == df.building_type.ScrewPump then
if direction == df.screw_pump_direction.FromSouth then
start_pos.y = start_pos.y + 1
elseif direction == df.screw_pump_direction.FromEast then
start_pos.x = start_pos.x + 1
end
end
local min_x, max_x = start_pos.x, start_pos.x
local min_y, max_y = start_pos.y, start_pos.y
local min_z, max_z = start_pos.z, start_pos.z
if adjusted_width == 1 and adjusted_height == 1
and (width > 1 or height > 1 or depth > 1) then
max_x = min_x + width - 1
max_y = min_y + height - 1
max_z = math.max(uibs.selection_pos.z, pos.z)
end
return {
p1=xyz2pos(min_x, min_y, min_z),
p2=xyz2pos(max_x, max_y, max_z),
width=adjusted_width,
height=adjusted_height
}
end
function PlannerOverlay:save_placement()
self.saved_placement = get_placement_data()
if (uibs.selection_pos:isValid()) then
self.saved_selection_pos_valid = true
self.saved_selection_pos = copyall(uibs.selection_pos)
self.saved_pos = copyall(uibs.pos)
uibs.selection_pos:clear()
else
self.saved_selection_pos = copyall(self.saved_placement.p1)
self.saved_pos = copyall(self.saved_placement.p2)
self.saved_pos.x = self.saved_pos.x + self.saved_placement.width - 1
self.saved_pos.y = self.saved_pos.y + self.saved_placement.height - 1
end
end
function PlannerOverlay:restore_placement()
if self.saved_selection_pos_valid then
uibs.selection_pos = self.saved_selection_pos
self.saved_selection_pos_valid = nil
else
uibs.selection_pos:clear()
end
self.saved_selection_pos = nil
self.saved_pos = nil
local placement_data = self.saved_placement
self.saved_placement = nil
return placement_data
end
2023-02-09 01:13:53 -07:00
function PlannerOverlay:onInput(keys)
if not is_plannable() then return false end
if keys.LEAVESCREEN or keys._MOUSE_R_DOWN then
if uibs.selection_pos:isValid() then
uibs.selection_pos:clear()
return true
end
2023-02-17 15:24:21 -07:00
self.selected = 1
self.subviews.hollow:setOption(false)
self.subviews.choose:setOption(false)
self:reset()
2023-02-19 01:57:30 -07:00
reset_counts_flag = true
2023-02-09 01:13:53 -07:00
return false
end
if PlannerOverlay.super.onInput(self, keys) then
return true
end
if keys._MOUSE_L_DOWN then
2023-02-11 03:10:07 -07:00
if is_over_options_panel() then return false end
local detect_rect = copyall(self.frame_rect)
detect_rect.height = self.subviews.main.frame_rect.height +
self.subviews.errors.frame_rect.height
detect_rect.y2 = detect_rect.y1 + detect_rect.height - 1
if self.subviews.main:getMousePos(gui.ViewRect{rect=detect_rect})
or self.subviews.errors:getMousePos() then
return true
end
2023-02-09 01:13:53 -07:00
if #uibs.errors > 0 then return true end
if dfhack.gui.getMousePos() then
2023-02-09 01:13:53 -07:00
if is_choosing_area() or cur_building_has_no_area() then
local filters = get_cur_filters()
local num_filters = #filters
2023-02-18 02:09:54 -07:00
if num_filters == 0 then
2023-02-09 01:13:53 -07:00
return false -- we don't add value; let the game place it
end
2023-02-18 02:09:54 -07:00
local choose = self.subviews.choose
if choose.enabled() and choose:getOptionValue() then
self:save_placement()
local is_hollow = self.subviews.hollow:getOptionValue()
2023-02-19 02:58:17 -07:00
local chosen_items, active_screens = {}, {}
2023-02-18 02:09:54 -07:00
local pending = num_filters
df.global.game.main_interface.bottom_mode_selected = -1
2023-02-18 02:09:54 -07:00
for idx = num_filters,1,-1 do
chosen_items[idx] = {}
if (self.subviews['item'..idx].available or 0) > 0 then
2023-02-19 02:58:17 -07:00
active_screens[idx] = ItemSelectionScreen{
index=idx,
quantity=get_quantity(filters[idx], is_hollow,
self.saved_placement),
2023-02-18 02:09:54 -07:00
on_submit=function(items)
chosen_items[idx] = items
2023-02-19 02:58:17 -07:00
active_screens[idx]:dismiss()
active_screens[idx] = nil
2023-02-18 02:09:54 -07:00
pending = pending - 1
if pending == 0 then
df.global.game.main_interface.bottom_mode_selected = df.main_bottom_mode_type.BUILDING_PLACEMENT
self:place_building(self:restore_placement(), chosen_items)
2023-02-18 02:09:54 -07:00
end
end,
2023-02-19 02:58:17 -07:00
on_cancel=function()
for i,scr in pairs(active_screens) do
scr:dismiss()
end
df.global.game.main_interface.bottom_mode_selected = df.main_bottom_mode_type.BUILDING_PLACEMENT
self:restore_placement()
2023-02-19 02:58:17 -07:00
end,
2023-02-18 02:09:54 -07:00
}:show()
else
pending = pending - 1
end
end
else
self:place_building(get_placement_data())
2023-02-18 02:09:54 -07:00
end
2023-02-09 01:13:53 -07:00
return true
elseif not is_choosing_area() then
return false
end
end
end
return keys._MOUSE_L
end
function PlannerOverlay:render(dc)
if not is_plannable() then return end
self.subviews.errors:updateLayout()
2023-02-09 01:13:53 -07:00
PlannerOverlay.super.render(self, dc)
end
local GOOD_PEN, BAD_PEN
function reload_cursors()
GOOD_PEN = to_pen{ch='o', fg=COLOR_GREEN, tile=dfhack.screen.findGraphicsTile('CURSORS', 1, 2)}
BAD_PEN = to_pen{ch='X', fg=COLOR_RED, tile=dfhack.screen.findGraphicsTile('CURSORS', 3, 0)}
end
reload_cursors()
2023-02-09 01:13:53 -07:00
function PlannerOverlay:onRenderFrame(dc, rect)
PlannerOverlay.super.onRenderFrame(self, dc, rect)
if reset_counts_flag then
self:reset()
2023-02-19 01:57:30 -07:00
self.subviews.safety:setOption(getHeatSafetyFilter(
uibs.building_type, uibs.building_subtype, uibs.custom_type))
end
local selection_pos = self.saved_selection_pos or uibs.selection_pos
if not selection_pos or selection_pos.x < 0 then return end
2023-02-09 01:13:53 -07:00
local pos = self.saved_pos or uibs.pos
2023-02-09 01:13:53 -07:00
local bounds = {
x1 = math.min(selection_pos.x, pos.x),
x2 = math.max(selection_pos.x, pos.x),
y1 = math.min(selection_pos.y, pos.y),
y2 = math.max(selection_pos.y, pos.y),
2023-02-09 01:13:53 -07:00
}
local hollow = self.subviews.hollow:getOptionValue()
local pen = (self.saved_selection_pos or #uibs.errors == 0) and GOOD_PEN or BAD_PEN
2023-02-09 01:13:53 -07:00
local function get_overlay_pen(pos)
if not hollow then return pen end
if pos.x == bounds.x1 or pos.x == bounds.x2 or
pos.y == bounds.y1 or pos.y == bounds.y2 then
return pen
end
return gui.TRANSPARENT_PEN
2023-02-09 01:13:53 -07:00
end
guidm.renderMapOverlay(get_overlay_pen, bounds)
end
function PlannerOverlay:get_stairs_subtype(pos, corner1, corner2)
local subtype = uibs.building_subtype
if pos.z == corner1.z then
local opt = self.subviews.stairs_bottom_subtype:getOptionValue()
if opt == 'auto' then
local tt = dfhack.maps.getTileType(pos)
local shape = df.tiletype.attrs[tt].shape
if shape ~= df.tiletype_shape.STAIR_DOWN then
subtype = df.construction_type.UpStair
end
else
subtype = opt
end
elseif pos.z == corner2.z then
local opt = self.subviews.stairs_top_subtype:getOptionValue()
if opt == 'auto' then
local tt = dfhack.maps.getTileType(pos)
local shape = df.tiletype.attrs[tt].shape
if shape ~= df.tiletype_shape.STAIR_UP then
subtype = df.construction_type.DownStair
end
else
subtype = opt
2023-02-11 03:10:07 -07:00
end
end
return subtype
end
function PlannerOverlay:place_building(placement_data, chosen_items)
local p1, p2 = placement_data.p1, placement_data.p2
2023-02-09 01:13:53 -07:00
local blds = {}
local hollow = self.subviews.hollow:getOptionValue()
2023-02-11 03:10:07 -07:00
local subtype = uibs.building_subtype
for z=p1.z,p2.z do for y=p1.y,p2.y do for x=p1.x,p2.x do
if hollow and x ~= p1.x and x ~= p2.x and y ~= p1.y and y ~= p2.y then
goto continue
end
2023-02-11 03:10:07 -07:00
local pos = xyz2pos(x, y, z)
if is_stairs() then
subtype = self:get_stairs_subtype(pos, p1, p2)
2023-02-11 03:10:07 -07:00
end
local bld, err = dfhack.buildings.constructBuilding{pos=pos,
type=uibs.building_type, subtype=subtype, custom=uibs.custom_type,
width=placement_data.width, height=placement_data.height,
direction=uibs.direction}
2023-02-09 01:13:53 -07:00
if err then
for _,b in ipairs(blds) do
dfhack.buildings.deconstruct(b)
end
2023-02-11 03:10:07 -07:00
dfhack.printerr(err .. (' (%d, %d, %d)'):format(pos.x, pos.y, pos.z))
2023-02-09 01:13:53 -07:00
return
end
-- assign fields for the types that need them. we can't pass them all in
-- to the call to constructBuilding since attempting to assign unrelated
-- fields to building types that don't support them causes errors.
for k,v in pairs(bld) do
if k == 'friction' then bld.friction = uibs.friction end
if k == 'use_dump' then bld.use_dump = uibs.use_dump end
if k == 'dump_x_shift' then bld.dump_x_shift = uibs.dump_x_shift end
if k == 'dump_y_shift' then bld.dump_y_shift = uibs.dump_y_shift end
if k == 'speed' then bld.speed = uibs.speed end
end
table.insert(blds, bld)
::continue::
2023-02-11 03:10:07 -07:00
end end end
self.subviews.item1:reduce_quantity()
self.subviews.item2:reduce_quantity()
self.subviews.item3:reduce_quantity()
self.subviews.item4:reduce_quantity()
2023-02-09 01:13:53 -07:00
for _,bld in ipairs(blds) do
2023-02-18 02:09:54 -07:00
-- attach chosen items and reduce job_item quantity
if chosen_items then
local job = bld.jobs[0]
local jitems = job.job_items
for idx=1,#get_cur_filters() do
local item_ids = chosen_items[idx]
while jitems[idx-1].quantity > 0 and #item_ids > 0 do
local item_id = item_ids[#item_ids]
local item = df.item.find(item_id)
if not item then
dfhack.printerr(('item no longer available: %d'):format(item_id))
break
end
if not dfhack.job.attachJobItem(job, item, df.job_item_ref.T_role.Hauled, idx-1, -1) then
dfhack.printerr(('cannot attach item: %d'):format(item_id))
break
end
jitems[idx-1].quantity = jitems[idx-1].quantity - 1
item_ids[#item_ids] = nil
end
end
end
2023-02-09 01:13:53 -07:00
addPlannedBuilding(bld)
end
2023-02-13 17:24:10 -07:00
scheduleCycle()
2023-02-18 02:09:54 -07:00
uibs.selection_pos:clear()
2023-02-09 01:13:53 -07:00
end
2023-02-15 20:10:42 -07:00
--------------------------------
2023-02-19 22:17:03 -07:00
-- InspectorLine
2023-02-15 20:10:42 -07:00
--
local function get_building_filters()
local bld = dfhack.gui.getSelectedBuilding()
return dfhack.buildings.getFiltersByType({},
bld:getType(), bld:getSubtype(), bld:getCustomType())
end
InspectorLine = defclass(InspectorLine, widgets.Panel)
InspectorLine.ATTRS{
idx=DEFAULT_NIL,
}
function InspectorLine:init()
self.frame.h = 2
self.visible = function() return #get_building_filters() >= self.idx end
self:addviews{
widgets.Label{
frame={t=0, l=0},
text={{text=self:callback('get_desc_string')}},
2023-02-15 20:10:42 -07:00
},
widgets.Label{
frame={t=1, l=2},
text={{text=self:callback('get_status_line')}},
},
}
end
function InspectorLine:get_desc_string()
if self.desc then return self.desc end
self.desc = getDescString(dfhack.gui.getSelectedBuilding(), self.idx-1)
return self.desc
end
2023-02-15 20:10:42 -07:00
function InspectorLine:get_status_line()
if self.status then return self.status end
2023-02-15 20:10:42 -07:00
local queue_pos = getQueuePosition(dfhack.gui.getSelectedBuilding(), self.idx-1)
if queue_pos <= 0 then
return 'Item attached'
end
self.status = ('Position in line: %d'):format(queue_pos)
return self.status
end
function InspectorLine:reset()
self.desc = nil
self.status = nil
2023-02-15 20:10:42 -07:00
end
2023-02-19 22:17:03 -07:00
--------------------------------
-- InspectorOverlay
--
InspectorOverlay = defclass(InspectorOverlay, overlay.OverlayWidget)
InspectorOverlay.ATTRS{
default_pos={x=-41,y=14},
default_enabled=true,
viewscreens='dwarfmode/ViewSheets/BUILDING',
2023-02-21 19:05:15 -07:00
frame={w=30, h=15},
frame_style=gui.MEDIUM_FRAME,
frame_background=gui.CLEAR_PEN,
}
function InspectorOverlay:init()
self:addviews{
widgets.Label{
frame={t=0, l=0},
text='Waiting for items:',
},
2023-02-15 20:10:42 -07:00
InspectorLine{view_id='item1', frame={t=2, l=0}, idx=1},
InspectorLine{view_id='item2', frame={t=4, l=0}, idx=2},
InspectorLine{view_id='item3', frame={t=6, l=0}, idx=3},
InspectorLine{view_id='item4', frame={t=8, l=0}, idx=4},
2023-02-11 03:21:19 -07:00
widgets.HotkeyLabel{
2023-02-21 19:05:15 -07:00
frame={t=11, l=0},
2023-02-11 03:21:19 -07:00
label='adjust filters',
key='CUSTOM_CTRL_F',
},
widgets.HotkeyLabel{
2023-02-21 19:05:15 -07:00
frame={t=12, l=0},
label='make top priority',
key='CUSTOM_CTRL_T',
on_activate=self:callback('make_top_priority'),
},
}
end
function InspectorOverlay:reset()
self.subviews.item1:reset()
self.subviews.item2:reset()
self.subviews.item3:reset()
self.subviews.item4:reset()
reset_inspector_flag = false
end
function InspectorOverlay:make_top_priority()
makeTopPriority(dfhack.gui.getSelectedBuilding())
self:reset()
end
function InspectorOverlay:onInput(keys)
if not isPlannedBuilding(dfhack.gui.getSelectedBuilding()) then
return false
end
if keys._MOUSE_L_DOWN or keys._MOUSE_R_DOWN or keys.LEAVESCREEN then
self:reset()
end
return InspectorOverlay.super.onInput(self, keys)
end
function InspectorOverlay:render(dc)
if not isPlannedBuilding(dfhack.gui.getSelectedBuilding()) then
return
end
if reset_inspector_flag then
self:reset()
end
InspectorOverlay.super.render(self, dc)
end
OVERLAY_WIDGETS = {
planner=PlannerOverlay,
inspector=InspectorOverlay,
}
return _ENV