local _ENV = mkmodule('plugins.buildingplan') --[[ Native functions: * bool isPlannableBuilding(df::building_type type, int16_t subtype, int32_t custom) * bool isPlannedBuilding(df::building *bld) * void addPlannedBuilding(df::building *bld) * void doCycle() * void scheduleCycle() --]] local argparse = require('argparse') 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') 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) 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 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 end local function get_quantity(filter, hollow, placement_data) local quantity = filter.quantity or 1 local dimx, dimy, dimz = get_cur_area_dims(placement_data) 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 end return quantity * dimx * dimy * dimz 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 function get_desc(filter) local desc = 'Unknown' if filter.has_tool_use and filter.has_tool_use > -1 then 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 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'; if filter.flags2.fire_safe then desc = 'Fire-safe material'; end if filter.flags2.magma_safe then desc = 'Magma-safe material'; end end if desc:endswith('s') then desc = desc:sub(1,-2) end if desc == 'Trappart' then desc = 'Mechanism' elseif desc == 'Wood' then desc = 'Log' end return desc end local BUTTON_START_PEN, BUTTON_END_PEN, SELECTED_ITEM_PEN = nil, nil, nil local reset_counts_flag = false local reset_inspector_flag = false function signal_reset() BUTTON_START_PEN = nil BUTTON_END_PEN = nil SELECTED_ITEM_PEN = nil reset_counts_flag = true reset_inspector_flag = true end 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 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 BuildingplanScreen = defclass(BuildingplanScreen, gui.ZScreen) BuildingplanScreen.ATTRS { pass_movement_keys=true, pass_mouse_clicks=false, defocusable=false, } -------------------------------- -- ItemSelection -- 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} -- 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 ItemSelection = defclass(ItemSelection, widgets.Window) ItemSelection.ATTRS{ frame_title='Choose items', frame={w=56, h=20, l=4, t=8}, resizable=true, index=DEFAULT_NIL, quantity=DEFAULT_NIL, on_submit=DEFAULT_NIL, on_cancel=DEFAULT_NIL, } function ItemSelection:init() local filter = get_cur_filters()[self.index] self.num_selected = 0 self.selected_set = {} local plural = self.quantity == 1 and '' or 's' self:addviews{ widgets.Label{ frame={t=0, l=0, r=10}, text={ get_desc(filter), plural, NEWLINE, ('Select up to %d item%s ('):format(self.quantity, plural), {text=function() return self.num_selected end}, ' selected)', }, }, 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'), }, widgets.FilteredList{ view_id='flist', frame={t=3, l=0, r=0, b=4}, case_sensitive=false, choices=self:get_choices(sort_by_recency), icon_width=2, on_submit=self:callback('toggle_group'), edit_on_char=function(ch) return ch:match('[%l -]') end, }, widgets.CycleHotkeyLabel{ frame={l=0, b=2}, key='CUSTOM_SHIFT_R', 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}, key='SELECT', label='Use all/none', auto_width=true, on_activate=function() self:toggle_group(self.subviews.flist.list:getSelected()) end, }, widgets.HotkeyLabel{ frame={l=22, b=1}, key='CUSTOM_SHIFT_B', label='Build', auto_width=true, on_activate=self:callback('submit'), }, widgets.HotkeyLabel{ frame={l=38, b=1}, key='LEAVESCREEN', label='Go back', auto_width=true, on_activate=self:callback('on_cancel'), }, widgets.HotkeyLabel{ frame={l=0, b=0}, key='KEYBOARD_CURSOR_RIGHT_FAST', key_sep=' : ', label='Use one', auto_width=true, on_activate=function() self:increment_group(self.subviews.flist.list:getSelected()) end, }, widgets.Label{ frame={l=6, b=0, w=5}, text_pen=COLOR_LIGHTGREEN, text='Right', }, widgets.HotkeyLabel{ frame={l=23, b=0}, key='KEYBOARD_CURSOR_LEFT_FAST', key_sep=' : ', label='Use one fewer', auto_width=true, on_activate=function() self:decrement_group(self.subviews.flist.list:getSelected()) end, }, widgets.Label{ frame={l=29, b=0, w=4}, text_pen=COLOR_LIGHTGREEN, text='Left', }, } end -- 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 local function make_search_key(str) local out = '' for c in str:gmatch("[%w%s]") do out = out .. c end return out end function ItemSelection:get_choices(sort_fn) local item_ids = getAvailableItems(uibs.building_type, uibs.building_subtype, uibs.custom_type, self.index-1) local buckets = {} 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] table.insert(bucket.data.item_ids, item_id) bucket.data.quantity = bucket.data.quantity + 1 else local entry = { search_key=make_search_key(desc), icon=self:callback('get_entry_icon', item_id), data={ item_ids={item_id}, item_type=item:getType(), item_subtype=item:getSubtype(), quantity=1, quality=item:getQuality(), selected=0, }, } buckets[desc] = entry end ::continue:: end local choices = {} 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}, } table.insert(choices, choice) end table.sort(choices, sort_fn) return choices end 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 end end function ItemSelection:get_entry_icon(item_id) return self.selected_set[item_id] and get_selected_item_pen() or nil end 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 function ItemSelection:submit() local selected_items = {} for item_id in pairs(self.selected_set) do table.insert(selected_items, item_id) end if #selected_items > 0 then track_recently_used(self.subviews.flist:getChoices()) end self.on_submit(selected_items) end function ItemSelection:onInput(keys) if keys.LEAVESCREEN or keys._MOUSE_R_DOWN then self.on_cancel() return true 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 end return ItemSelection.super.onInput(self, keys) end ItemSelectionScreen = defclass(ItemSelectionScreen, BuildingplanScreen) ItemSelectionScreen.ATTRS { focus_path='dwarfmode/Building/Placement/dfhack/lua/buildingplan/itemselection', force_pause=true, pass_pause=false, index=DEFAULT_NIL, quantity=DEFAULT_NIL, on_submit=DEFAULT_NIL, on_cancel=DEFAULT_NIL, } function ItemSelectionScreen:init() self:addviews{ ItemSelection{ index=self.index, quantity=self.quantity, on_submit=self.on_submit, on_cancel=self.on_cancel, } } end -------------------------------- -- Slider -- Slider = defclass(Slider, widgets.Widget) Slider.ATTRS{ num_stops=DEFAULT_NIL, get_left_idx_fn=DEFAULT_NIL, get_right_idx_fn=DEFAULT_NIL, on_left_change=DEFAULT_NIL, on_right_change=DEFAULT_NIL, } function Slider:preinit(init_table) init_table.frame = init_table.frame or {} init_table.frame.h = init_table.frame.h or 1 end function Slider:init() if self.num_stops < 2 then error('too few Slider stops') end self.is_dragging_target = nil -- 'left', 'right', or 'both' self.is_dragging_idx = nil -- offset from leftmost dragged tile end local function slider_get_width_per_idx(self) return math.max(5, (self.frame_body.width-7) // (self.num_stops-1)) end function Slider:onInput(keys) if not keys._MOUSE_L_DOWN then return false end local x = self:getMousePos() if not x then return false end local left_idx, right_idx = self.get_left_idx_fn(), self.get_right_idx_fn() local width_per_idx = slider_get_width_per_idx(self) local left_pos = width_per_idx*(left_idx-1) local right_pos = width_per_idx*(right_idx-1) + 4 if x < left_pos then self.on_left_change(self.get_left_idx_fn() - 1) elseif x < left_pos+3 then self.is_dragging_target = 'left' self.is_dragging_idx = x - left_pos elseif x < right_pos then self.is_dragging_target = 'both' self.is_dragging_idx = x - left_pos elseif x < right_pos+3 then self.is_dragging_target = 'right' self.is_dragging_idx = x - right_pos else self.on_right_change(self.get_right_idx_fn() + 1) end return true end local function slider_do_drag(self, width_per_idx) local x = self.frame_body:localXY(dfhack.screen.getMousePos()) local cur_pos = x - self.is_dragging_idx cur_pos = math.max(0, cur_pos) cur_pos = math.min(width_per_idx*(self.num_stops-1)+7, cur_pos) local offset = self.is_dragging_target == 'right' and -2 or 1 local new_idx = math.max(0, cur_pos+offset)//width_per_idx + 1 local new_left_idx, new_right_idx if self.is_dragging_target == 'right' then new_right_idx = new_idx else new_left_idx = new_idx if self.is_dragging_target == 'both' then new_right_idx = new_left_idx + self.get_right_idx_fn() - self.get_left_idx_fn() if new_right_idx > self.num_stops then return end end end if new_left_idx and new_left_idx ~= self.get_left_idx_fn() then self.on_left_change(new_left_idx) end if new_right_idx and new_right_idx ~= self.get_right_idx_fn() then self.on_right_change(new_right_idx) end end local SLIDER_LEFT_END = to_pen{ch=198, fg=COLOR_GREY, bg=COLOR_BLACK} local SLIDER_TRACK = to_pen{ch=205, fg=COLOR_GREY, bg=COLOR_BLACK} local SLIDER_TRACK_SELECTED = to_pen{ch=205, fg=COLOR_LIGHTGREEN, bg=COLOR_BLACK} local SLIDER_TRACK_STOP = to_pen{ch=216, fg=COLOR_GREY, bg=COLOR_BLACK} local SLIDER_TRACK_STOP_SELECTED = to_pen{ch=216, fg=COLOR_LIGHTGREEN, bg=COLOR_BLACK} local SLIDER_RIGHT_END = to_pen{ch=181, fg=COLOR_GREY, bg=COLOR_BLACK} local SLIDER_TAB_LEFT = to_pen{ch=60, fg=COLOR_BLACK, bg=COLOR_YELLOW} local SLIDER_TAB_CENTER = to_pen{ch=9, fg=COLOR_BLACK, bg=COLOR_YELLOW} local SLIDER_TAB_RIGHT = to_pen{ch=62, fg=COLOR_BLACK, bg=COLOR_YELLOW} function Slider:onRenderBody(dc, rect) local left_idx, right_idx = self.get_left_idx_fn(), self.get_right_idx_fn() local width_per_idx = slider_get_width_per_idx(self) -- draw track dc:seek(1,0) dc:char(nil, SLIDER_LEFT_END) dc:char(nil, SLIDER_TRACK) for stop_idx=1,self.num_stops-1 do local track_stop_pen = SLIDER_TRACK_STOP_SELECTED local track_pen = SLIDER_TRACK_SELECTED if left_idx > stop_idx or right_idx < stop_idx then track_stop_pen = SLIDER_TRACK_STOP track_pen = SLIDER_TRACK elseif right_idx == stop_idx then track_pen = SLIDER_TRACK end dc:char(nil, track_stop_pen) for i=2,width_per_idx do dc:char(nil, track_pen) end end if right_idx >= self.num_stops then dc:char(nil, SLIDER_TRACK_STOP_SELECTED) else dc:char(nil, SLIDER_TRACK_STOP) end dc:char(nil, SLIDER_TRACK) dc:char(nil, SLIDER_RIGHT_END) -- draw tabs dc:seek(width_per_idx*(left_idx-1)) dc:char(nil, SLIDER_TAB_LEFT) dc:char(nil, SLIDER_TAB_CENTER) dc:char(nil, SLIDER_TAB_RIGHT) dc:seek(width_per_idx*(right_idx-1)+4) dc:char(nil, SLIDER_TAB_LEFT) dc:char(nil, SLIDER_TAB_CENTER) dc:char(nil, SLIDER_TAB_RIGHT) -- manage dragging if self.is_dragging_target then slider_do_drag(self, width_per_idx) end if df.global.enabler.mouse_lbut == 0 then self.is_dragging_target = nil self.is_dragging_idx = nil end end -------------------------------- -- QualityAndMaterialsPage -- QualityAndMaterialsPage = defclass(QualityAndMaterialsPage, widgets.Panel) QualityAndMaterialsPage.ATTRS{ frame={t=0, l=0}, index=DEFAULT_NIL, } local TYPE_COL_WIDTH = 20 local HEADER_HEIGHT = 7 local QUALITY_HEIGHT = 9 local FOOTER_HEIGHT = 4 -- 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 local function mat_sort_by_name(a, b) return a.name < b.name end local function mat_sort_by_quantity(a, b) return a.quantity > b.quantity or (a.quantity == b.quantity and mat_sort_by_name(a, b)) end function QualityAndMaterialsPage:init() self.dirty = true self.summary = '' local enable_item_quality = can_be_improved(self.index) self:addviews{ widgets.Panel{ view_id='header', frame={l=0, t=0, h=HEADER_HEIGHT, r=0}, frame_inset={l=1}, subviews={ widgets.Label{ frame={l=0, t=0}, text='Current filter:', }, widgets.WrappedLabel{ frame={l=16, t=0, h=2, r=0}, text_pen=COLOR_LIGHTCYAN, text_to_wrap=function() return self.summary end, auto_height=false, }, widgets.CycleHotkeyLabel{ view_id='mat_sort', frame={l=0, t=3, w=21}, label='Sort by:', key='CUSTOM_SHIFT_R', options={ {label='name', value=mat_sort_by_name}, {label='available', value=mat_sort_by_quantity} }, on_change=function() self.dirty = true end, }, widgets.ToggleHotkeyLabel{ view_id='hide_zero', frame={l=0, t=4, w=24}, label='Hide unavailable:', key='CUSTOM_SHIFT_H', initial_option=false, on_change=function() self.dirty = true end, }, widgets.EditField{ view_id='search', frame={l=26, t=3}, label_text='Search: ', on_char=function(ch) return ch:match('[%l -]') end, }, 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', frame={l=0, t=HEADER_HEIGHT, r=0, b=FOOTER_HEIGHT+QUALITY_HEIGHT}, frame_style=gui.INTERIOR_FRAME, subviews={ widgets.List{ view_id='materials_categories', frame={l=1, t=0, b=0, w=TYPE_COL_WIDTH-3}, scroll_keys={}, icon_width=2, cursor_pen=COLOR_CYAN, on_submit=self:callback('toggle_category'), }, widgets.FilteredList{ view_id='materials_mats', frame={l=TYPE_COL_WIDTH, t=0, r=0, b=0}, icon_width=2, on_submit=self:callback('toggle_material'), }, }, }, widgets.Panel{ view_id='divider', frame={l=TYPE_COL_WIDTH-1, t=HEADER_HEIGHT, b=FOOTER_HEIGHT+QUALITY_HEIGHT, w=1}, on_render=self:callback('draw_divider'), }, widgets.Panel{ view_id='quality_panel', frame={l=0, r=0, h=QUALITY_HEIGHT, b=FOOTER_HEIGHT}, frame_style=gui.INTERIOR_FRAME, frame_title='Item quality', subviews={ widgets.CycleHotkeyLabel{ view_id='decorated', frame={l=0, t=1, w=23}, key='CUSTOM_SHIFT_D', label='Decorated only:', options={ {label='No', value=false}, {label='Yes', value=true}, }, enabled=enable_item_quality, on_change=self:callback('set_decorated'), }, widgets.CycleHotkeyLabel{ view_id='min_quality', frame={l=0, t=3, w=18}, label='Min quality:', label_below=true, key_back='CUSTOM_SHIFT_Z', key='CUSTOM_SHIFT_X', options={ {label='Ordinary', value=0}, {label='Well Crafted', value=1}, {label='Finely Crafted', value=2}, {label='Superior', value=3}, {label='Exceptional', value=4}, {label='Masterful', value=5}, {label='Artifact', value=6}, }, enabled=enable_item_quality, on_change=function(val) self:set_min_quality(val+1) end, }, widgets.CycleHotkeyLabel{ view_id='max_quality', frame={r=1, t=3, w=18}, label='Max quality:', label_below=true, key_back='CUSTOM_SHIFT_Q', key='CUSTOM_SHIFT_W', options={ {label='Ordinary', value=0}, {label='Well Crafted', value=1}, {label='Finely Crafted', value=2}, {label='Superior', value=3}, {label='Exceptional', value=4}, {label='Masterful', value=5}, {label='Artifact', value=6}, }, enabled=enable_item_quality, on_change=function(val) self:set_max_quality(val+1) end, }, Slider{ frame={l=0, t=6}, num_stops=7, get_left_idx_fn=function() return self.subviews.min_quality:getOptionValue() + 1 end, get_right_idx_fn=function() return self.subviews.max_quality:getOptionValue() + 1 end, on_left_change=self:callback('set_min_quality'), on_right_change=self:callback('set_max_quality'), active=enable_item_quality, }, }, }, widgets.Panel{ view_id='footer', frame={l=0, r=0, b=0, h=FOOTER_HEIGHT}, frame_inset={t=1, l=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='Invert selection', auto_width=true, key='CUSTOM_SHIFT_I', on_activate=self:callback('invert_materials'), }, widgets.HotkeyLabel{ frame={l=30, t=2}, label='Reset filter', auto_width=true, key='CUSTOM_SHIFT_X', on_activate=self:callback('clear_filter'), }, }, } } -- replace the FilteredList's built-in EditField with our own self.subviews.materials_mats.list.frame.t = 0 self.subviews.materials_mats.edit.visible = false self.subviews.materials_mats.edit = self.subviews.search self.subviews.search.on_change = self.subviews.materials_mats:callback('onFilterChange') end local MAT_ENABLED_PEN = to_pen{ch=string.char(251), fg=COLOR_LIGHTGREEN} local MAT_DISABLED_PEN = to_pen{ch='x', fg=COLOR_RED} local function make_cat_choice(label, cat, key, cats) local enabled = cats[cat] local icon = nil if not cats.unset then icon = enabled and MAT_ENABLED_PEN or MAT_DISABLED_PEN end return { text=label, key=key, enabled=enabled, cat=cat, icon=icon, } end local function make_mat_choice(name, props, enabled, cats) local quantity = tonumber(props.count) local text = ('%5d - %s'):format(quantity, name) local icon = nil if not cats.unset then icon = enabled and MAT_ENABLED_PEN or MAT_DISABLED_PEN end return { text=text, enabled=enabled, icon=icon, name=name, cat=props.category, quantity=quantity, } end function QualityAndMaterialsPage:refresh() local summary = get_desc(get_cur_filters()[self.index]) local subviews = self.subviews local heat = getHeatSafetyFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type) if heat >= 2 then summary = 'Magma safe ' .. summary elseif heat == 1 then summary = 'Fire safe ' .. summary else summary = 'Any ' .. summary end local quality = getQualityFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, self.index-1) subviews.decorated:setOption(quality.decorated ~= 0) subviews.min_quality:setOption(quality.min_quality) subviews.max_quality:setOption(quality.max_quality) local cats = getMaterialMaskFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, self.index-1) local category_choices={ make_cat_choice('Stone', 'stone', 'CUSTOM_SHIFT_S', cats), make_cat_choice('Wood', 'wood', 'CUSTOM_SHIFT_O', cats), make_cat_choice('Metal', 'metal', 'CUSTOM_SHIFT_M', cats), make_cat_choice('Glass', 'glass', 'CUSTOM_SHIFT_G', cats), } self.subviews.materials_categories:setChoices(category_choices) local mats = getMaterialFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, self.index-1) local mat_choices = {} local hide_zero = self.subviews.hide_zero:getOptionValue() local enabled_mat_names = {} for name,props in pairs(mats) do local enabled = props.enabled == 'true' and cats[props.category] if not cats.unset and enabled then table.insert(enabled_mat_names, name) end if not hide_zero or tonumber(props.count) > 0 then table.insert(mat_choices, make_mat_choice(name, props, enabled, cats)) end end table.sort(mat_choices, self.subviews.mat_sort:getOptionValue()) local prev_filter = self.subviews.search.text self.subviews.materials_mats:setChoices(mat_choices) self.subviews.materials_mats:setFilter(prev_filter) if #enabled_mat_names > 0 then table.sort(enabled_mat_names) summary = summary .. (' of %s'):format(table.concat(enabled_mat_names, ', ')) end self.summary = summary self.dirty = false self:updateLayout() end function QualityAndMaterialsPage:toggle_category(_, choice) local cats = {} if not choice.icon then -- toggling from unset to something is set table.insert(cats, choice.cat) else choice.enabled = not choice.enabled for _,c in ipairs(self.subviews.materials_categories:getChoices()) do if c.enabled then table.insert(cats, c.cat) end end end setMaterialMaskFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, self.index-1, cats) self.dirty = true end function QualityAndMaterialsPage:toggle_material(_, choice) local mats = {} if not choice.icon then -- toggling from unset to something is set table.insert(mats, choice.name) else for _,c in ipairs(self.subviews.materials_mats:getChoices()) do local enabled = c.enabled if choice.name == c.name then enabled = not c.enabled end if enabled then table.insert(mats, c.name) end end end setMaterialFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, self.index-1, mats) self.dirty = true end function QualityAndMaterialsPage:invert_materials() local mats = {} for _,c in ipairs(self.subviews.materials_mats:getChoices()) do if not c.icon then return end if not c.enabled then table.insert(mats, c.name) end end setMaterialFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, self.index-1, mats) self.dirty = true end function QualityAndMaterialsPage:clear_filter() clearFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, self.index-1) self.dirty = true end function QualityAndMaterialsPage:set_decorated(decorated) local subviews = self.subviews setQualityFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, self.index-1, decorated and 1 or 0, subviews.min_quality:getOptionValue(), subviews.max_quality:getOptionValue()) self.dirty = true end function QualityAndMaterialsPage:set_min_quality(idx) idx = math.min(6, math.max(0, idx-1)) local subviews = self.subviews subviews.min_quality:setOption(idx) if subviews.max_quality:getOptionValue() < idx then subviews.max_quality:setOption(idx) end setQualityFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, self.index-1, subviews.decorated:getOptionValue() and 1 or 0, idx, subviews.max_quality:getOptionValue()) self.dirty = true end function QualityAndMaterialsPage:set_max_quality(idx) idx = math.min(6, math.max(0, idx-1)) local subviews = self.subviews subviews.max_quality:setOption(idx) if subviews.min_quality:getOptionValue() > idx then subviews.min_quality:setOption(idx) end setQualityFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, self.index-1, subviews.decorated:getOptionValue() and 1 or 0, subviews.min_quality:getOptionValue(), idx) self.dirty = true end 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 QualityAndMaterialsPage: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 function QualityAndMaterialsPage:onRenderFrame(dc, rect) QualityAndMaterialsPage.super.onRenderFrame(self, dc, rect) if self.dirty then self:refresh() end end -------------------------------- -- GlobalSettingsPage -- GlobalSettingsPage = defclass(GlobalSettingsPage, widgets.ResizingPanel) GlobalSettingsPage.ATTRS{ autoarrange_subviews=true, frame={t=0, l=0}, frame_style=gui.INTERIOR_FRAME, } function GlobalSettingsPage:init() self:addviews{ widgets.WrappedLabel{ frame={l=0}, text_to_wrap='These options will affect the selection of "Generic Materials" for all future buildings.', }, widgets.Panel{ frame={h=1}, }, widgets.ToggleHotkeyLabel{ view_id='blocks', frame={l=0}, key='CUSTOM_B', label='Blocks', label_width=8, on_change=self:callback('update_setting', 'blocks'), }, widgets.ToggleHotkeyLabel{ view_id='logs', frame={l=0}, key='CUSTOM_L', label='Logs', label_width=8, on_change=self:callback('update_setting', 'logs'), }, widgets.ToggleHotkeyLabel{ view_id='boulders', frame={l=0}, key='CUSTOM_O', label='Boulders', label_width=8, on_change=self:callback('update_setting', 'boulders'), }, widgets.ToggleHotkeyLabel{ view_id='bars', frame={l=0}, key='CUSTOM_R', label='Bars', label_width=8, on_change=self:callback('update_setting', 'bars'), }, } self:init_settings() end function GlobalSettingsPage:init_settings() local settings = getGlobalSettings() local subviews = self.subviews subviews.blocks:setOption(settings.blocks) subviews.logs:setOption(settings.logs) subviews.boulders:setOption(settings.boulders) subviews.bars:setOption(settings.bars) end function GlobalSettingsPage:update_setting(setting, val) dfhack.run_command('buildingplan', 'set', setting, tostring(val)) self:init_settings() end -------------------------------- -- FilterSelection -- FilterSelection = defclass(FilterSelection, widgets.Window) FilterSelection.ATTRS{ frame_title='Choose filters', frame={w=55, h=53, l=30, t=8}, frame_inset={t=1}, resizable=true, index=DEFAULT_NIL, autoarrange_subviews=true, } function FilterSelection:init() self:addviews{ widgets.TabBar{ frame={t=0}, labels={ 'Quality and materials', 'Global settings', }, on_select=function(idx) self.subviews.pages:setSelected(idx) self:updateLayout() end, get_cur_page=function() return self.subviews.pages:getSelected() end, key='CUSTOM_CTRL_T', }, widgets.Widget{ frame={h=1}, }, widgets.Pages{ view_id='pages', frame={t=5, l=0, b=0, r=0}, subviews={ QualityAndMaterialsPage{index=self.index}, GlobalSettingsPage{}, }, }, } end FilterSelectionScreen = defclass(FilterSelectionScreen, BuildingplanScreen) FilterSelectionScreen.ATTRS { focus_path='dwarfmode/Building/Placement/dfhack/lua/buildingplan/filterselection', index=DEFAULT_NIL, } function FilterSelectionScreen:init() self:addviews{ FilterSelection{index=self.index} } end function FilterSelectionScreen:onShow() -- don't let the building "shadow" follow the mouse cursor while this screen is open df.global.game.main_interface.bottom_mode_selected = -1 end function FilterSelectionScreen:onDismiss() -- re-enable building shadow df.global.game.main_interface.bottom_mode_selected = df.main_bottom_mode_type.BUILDING_PLACEMENT end -------------------------------- -- ItemLine -- 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() return uibs.building_type == df.building_type.Construction end local function is_stairs() return is_construction() and uibs.building_subtype == df.construction_type.UpDownStair end 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 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} local rect = gui.mkdims_wh(0, 0, dfhack.screen.getWindowSize()) v:updateLayout(gui.ViewRect{rect=rect}) return v:getMousePos() end ItemLine = defclass(ItemLine, widgets.Panel) ItemLine.ATTRS{ idx=DEFAULT_NIL, is_selected_fn=DEFAULT_NIL, is_hollow_fn=DEFAULT_NIL, 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{ frame={t=0, l=0}, text='*', auto_width=true, visible=self.is_selected_fn, }, widgets.Label{ frame={t=0, l=25}, text={ {tile=get_button_start_pen}, {gap=6, tile=get_button_end_pen}, }, auto_width=true, on_click=function() self.on_filter(self.idx) end, }, widgets.Label{ frame={t=0, l=33}, text={ {tile=get_button_start_pen}, {gap=1, tile=get_button_end_pen}, }, auto_width=true, on_click=function() self.on_clear_filter(self.idx) end, }, widgets.Label{ frame={t=0, l=2}, text={ {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')}, {gap=3, text=function() return self.note end, pen=function() return self.note_pen end}, }, }, } end function ItemLine:reset() self.desc = nil self.available = nil end 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 COLOR_GREEN or COLOR_GREY end 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, uibs.building_subtype, uibs.custom_type, idx - 1) 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 return ('%d %s%s'):format(quantity, self.desc, quantity == 1 and '' or 's') end function ItemLine:reduce_quantity(used_quantity) if not self.available then return end local filter = get_cur_filters()[self.idx] used_quantity = used_quantity or get_quantity(filter, self.is_hollow_fn()) self.available = math.max(0, self.available - used_quantity) 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 -------------------------------- -- PlannerOverlay -- PlannerOverlay = defclass(PlannerOverlay, overlay.OverlayWidget) PlannerOverlay.ATTRS{ default_pos={x=5,y=9}, default_enabled=true, viewscreens='dwarfmode/Building/Placement', frame={w=56, h=20}, } function PlannerOverlay:init() 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, } 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{ widgets.Label{ frame={}, auto_width=true, text='No items required.', visible=function() return #get_cur_filters() == 0 end, }, 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'), 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'), 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'), 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'), 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}, }, }, widgets.CycleHotkeyLabel{ view_id='stairs_top_subtype', frame={t=4, l=4}, key='CUSTOM_R', label='Top Stair Type: ', visible=is_stairs, options={ {label='Auto', value='auto'}, {label='UpDown', value=df.construction_type.UpDownStair}, {label='Down', value=df.construction_type.DownStair}, }, }, widgets.CycleHotkeyLabel { view_id='stairs_bottom_subtype', frame={t=5, l=4}, key='CUSTOM_B', label='Bottom Stair Type: ', visible=is_stairs, options={ {label='Auto', value='auto'}, {label='UpDown', value=df.construction_type.UpDownStair}, {label='Up', value=df.construction_type.UpStair}, }, }, widgets.Label{ frame={b=3, l=17}, text={ 'Selected area: ', {text=function() return ('%dx%dx%d'):format(get_cur_area_dims(self.saved_placement)) end }, }, visible=function() return not cur_building_has_no_area() and (self.saved_placement or is_choosing_area()) end, }, 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, }, widgets.HotkeyLabel{ frame={b=1, l=21}, key='CUSTOM_F', label='Set filter', auto_width=true, on_activate=function() self:set_filter(self.selected) end, }, widgets.HotkeyLabel{ frame={b=1, l=37}, key='CUSTOM_X', label='Clear filter', auto_width=true, 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 }, widgets.CycleHotkeyLabel{ view_id='choose', frame={b=0, l=0, w=25}, key='CUSTOM_I', 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, }, widgets.CycleHotkeyLabel{ view_id='safety', frame={b=0, l=29, w=25}, key='CUSTOM_G', label='Building safety:', options={ {label='Any', value=0}, {label='Magma', value=2, pen=COLOR_RED}, {label='Fire', value=1, pen=COLOR_LIGHTRED}, }, initial_option=0, on_change=function(heat) setHeatSafetyFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, heat) end, }, }, }, } local error_panel = widgets.ResizingPanel{ view_id='errors', frame={t=14, l=0, r=0}, frame_style=gui.MEDIUM_FRAME, frame_background=gui.CLEAR_PEN, } error_panel:addviews{ widgets.WrappedLabel{ frame={t=0, l=0, r=0}, text_pen=COLOR_LIGHTRED, text_to_wrap=get_placement_errors, visible=function() return #uibs.errors > 0 end, }, widgets.Label{ 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 end function PlannerOverlay:set_filter(idx) FilterSelectionScreen{index=idx}:show() end function PlannerOverlay:clear_filter(idx) clearFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, idx-1) 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 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 self.selected = 1 self.subviews.hollow:setOption(false) self.subviews.choose:setOption(false) self:reset() reset_counts_flag = true return false end if PlannerOverlay.super.onInput(self, keys) then return true end if keys._MOUSE_L_DOWN then 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 if not is_construction() and #uibs.errors > 0 then return true end if dfhack.gui.getMousePos() then if is_choosing_area() or cur_building_has_no_area() then local filters = get_cur_filters() local num_filters = #filters if num_filters == 0 then return false -- we don't add value; let the game place it end local choose = self.subviews.choose if choose.enabled() and choose:getOptionValue() then self:save_placement() local is_hollow = self.subviews.hollow:getOptionValue() local chosen_items, active_screens = {}, {} local pending = num_filters df.global.game.main_interface.bottom_mode_selected = -1 for idx = num_filters,1,-1 do chosen_items[idx] = {} if (self.subviews['item'..idx].available or 0) > 0 then active_screens[idx] = ItemSelectionScreen{ index=idx, quantity=get_quantity(filters[idx], is_hollow, self.saved_placement), on_submit=function(items) chosen_items[idx] = items active_screens[idx]:dismiss() active_screens[idx] = nil 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) end end, 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() end, }:show() else pending = pending - 1 end end else self:place_building(get_placement_data()) end return true elseif not is_choosing_area() then return false end end end return keys._MOUSE_L or keys.SELECT end function PlannerOverlay:render(dc) if not is_plannable() then return end self.subviews.errors:updateLayout() 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() local ONE_BY_ONE = xy2pos(1, 1) function PlannerOverlay:onRenderFrame(dc, rect) PlannerOverlay.super.onRenderFrame(self, dc, rect) if reset_counts_flag then self:reset() 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 local pos = self.saved_pos or uibs.pos local bounds = { x1 = math.max(0, math.min(selection_pos.x, pos.x)), x2 = math.min(df.global.world.map.x_count-1, math.max(selection_pos.x, pos.x)), y1 = math.max(0, math.min(selection_pos.y, pos.y)), y2 = math.min(df.global.world.map.y_count-1, math.max(selection_pos.y, pos.y)), } local hollow = self.subviews.hollow:getOptionValue() local default_pen = (self.saved_selection_pos or #uibs.errors == 0) and GOOD_PEN or BAD_PEN local get_pen_fn = is_construction() and function(pos) return dfhack.buildings.checkFreeTiles(pos, ONE_BY_ONE) and GOOD_PEN or BAD_PEN end or function() return default_pen end local function get_overlay_pen(pos) if not hollow then return get_pen_fn(pos) end if pos.x == bounds.x1 or pos.x == bounds.x2 or pos.y == bounds.y1 or pos.y == bounds.y2 then return get_pen_fn(pos) end return gui.TRANSPARENT_PEN 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 end end return subtype end function PlannerOverlay:place_building(placement_data, chosen_items) local p1, p2 = placement_data.p1, placement_data.p2 local blds = {} local hollow = self.subviews.hollow:getOptionValue() 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 local pos = xyz2pos(x, y, z) if is_stairs() then subtype = self:get_stairs_subtype(pos, p1, p2) 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} if err then -- it's ok if some buildings fail to build goto continue 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:: end end end local used_quantity = is_construction() and #blds or false self.subviews.item1:reduce_quantity(used_quantity) self.subviews.item2:reduce_quantity(used_quantity) self.subviews.item3:reduce_quantity(used_quantity) self.subviews.item4:reduce_quantity(used_quantity) for _,bld in ipairs(blds) do -- 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 addPlannedBuilding(bld) end scheduleCycle() uibs.selection_pos:clear() end -------------------------------- -- InspectorLine -- 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')}}, }, 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 function InspectorLine:get_status_line() if self.status then return self.status end 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 end -------------------------------- -- InspectorOverlay -- InspectorOverlay = defclass(InspectorOverlay, overlay.OverlayWidget) InspectorOverlay.ATTRS{ default_pos={x=-41,y=14}, default_enabled=true, viewscreens='dwarfmode/ViewSheets/BUILDING', 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:', }, 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}, widgets.HotkeyLabel{ frame={t=11, l=0}, label='adjust filters', key='CUSTOM_CTRL_F', visible=false, -- until implemented }, widgets.HotkeyLabel{ 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 local RESUME_BUTTON_FRAME = {t=15, h=3, r=73, w=25} local function mouse_is_over_resume_button(rect) local x,y = dfhack.screen.getMousePos() if not x then return false end if y < RESUME_BUTTON_FRAME.t or y > RESUME_BUTTON_FRAME.t + RESUME_BUTTON_FRAME.h - 1 then return false end if x > rect.x2 - RESUME_BUTTON_FRAME.r + 1 or x < rect.x2 - RESUME_BUTTON_FRAME.r - RESUME_BUTTON_FRAME.w + 2 then return false end return true end function InspectorOverlay:onInput(keys) if not isPlannedBuilding(dfhack.gui.getSelectedBuilding()) then return false end if keys._MOUSE_L_DOWN and mouse_is_over_resume_button(self.frame_parent_rect) then return true elseif 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