local _ENV = mkmodule('plugins.buildingplan.planneroverlay') local itemselection = require('plugins.buildingplan.itemselection') local filterselection = require('plugins.buildingplan.filterselection') local gui = require('gui') local guidm = require('gui.dwarfmode') local json = require('json') local overlay = require('plugins.overlay') local pens = require('plugins.buildingplan.pens') local utils = require('utils') local widgets = require('gui.widgets') require('dfhack.buildings') config = config or json.open('dfhack-config/buildingplan.json') local uibs = df.global.buildreq reset_counts_flag = false 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 -- TODO: reuse data in quickfort database local function get_selection_size_limits() local btype = uibs.building_type if btype == df.building_type.Bridge or btype == df.building_type.FarmPlot or btype == df.building_type.RoadPaved or btype == df.building_type.RoadDirt then return {w=31, h=31} elseif btype == df.building_type.AxleHorizontal then return uibs.direction == 1 and {w=1, h=31} or {w=31, h=1} elseif btype == df.building_type.Rollers then return (uibs.direction == 1 or uibs.direction == 3) and {w=31, h=1} or {w=1, h=31} end end local function get_selected_bounds(selection_pos, pos) selection_pos = selection_pos or uibs.selection_pos if not is_choosing_area() then return end pos = pos or uibs.pos 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), z1=math.min(selection_pos.z, pos.z), z2=math.max(selection_pos.z, pos.z), } -- clamp to map edges bounds = { x1=math.max(0, bounds.x1), x2=math.min(df.global.world.map.x_count-1, bounds.x2), y1=math.max(0, bounds.y1), y2=math.min(df.global.world.map.y_count-1, bounds.y2), z1=math.max(0, bounds.z1), z2=math.min(df.global.world.map.z_count-1, bounds.z2), } local limits = get_selection_size_limits() if limits then -- clamp to building type area limit bounds = { x1=math.max(selection_pos.x - (limits.w-1), bounds.x1), x2=math.min(selection_pos.x + (limits.w-1), bounds.x2), y1=math.max(selection_pos.y - (limits.h-1), bounds.y1), y2=math.min(selection_pos.y + (limits.h-1), bounds.y2), z1=bounds.z1, z2=bounds.z2, } end return bounds end local function get_cur_area_dims(bounds) if not bounds and not is_choosing_area() then return 1, 1, 1 end bounds = bounds or get_selected_bounds() if not bounds then return 1, 1, 1 end return bounds.x2 - bounds.x1 + 1, bounds.y2 - bounds.y1 + 1, bounds.z2 - bounds.z1 + 1 end local function is_pressure_plate() return uibs.building_type == df.building_type.Trap and uibs.building_subtype == df.trap_type.PressurePlate end local function is_weapon_trap() return uibs.building_type == df.building_type.Trap and uibs.building_subtype == df.trap_type.WeaponTrap end local function is_spike_trap() return uibs.building_type == df.building_type.Weapon end local function is_weapon_or_spike_trap() return is_weapon_trap() or is_spike_trap() end -- adjusted from CycleHotkeyLabel on the planner panel local weapon_quantity = 1 -- TODO: this should account for erroring constructions local function get_quantity(filter, hollow, bounds) if is_pressure_plate() then local flags = uibs.plate_info.flags return (flags.units and 1 or 0) + (flags.water and 1 or 0) + (flags.magma and 1 or 0) + (flags.track and 1 or 0) elseif (is_weapon_trap() and filter.vector_id == df.job_item_vector_id.ANY_WEAPON) or is_spike_trap() then return weapon_quantity end local quantity = filter.quantity or 1 local dimx, dimy, dimz = get_cur_area_dims(bounds) 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 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_construction() return uibs.building_type == df.building_type.Construction end local function is_tutorial_open() local help = df.global.game.main_interface.help return help.open and help.context == df.help_context_type.START_TUTORIAL_WORKSHOPS_AND_TASKS end local function is_plannable() return not is_tutorial_open() and get_cur_filters() and not (is_construction() and uibs.building_subtype == df.construction_type.TrackNSEW) end local function is_slab() return uibs.building_type == df.building_type.Slab end local function is_cage() return uibs.building_type == df.building_type.Cage 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 is_pressure_plate() 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 -- 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 = 2 self.visible = function() return #get_cur_filters() >= self.idx end self:addviews{ widgets.Label{ view_id='item_symbol', frame={t=0, l=0}, text=string.char(16), -- this is the "►" character text_pen=COLOR_YELLOW, auto_width=true, visible=self.is_selected_fn, }, widgets.Label{ view_id='item_desc', frame={t=0, l=2}, text={ {text=self:callback('get_item_line_text'), pen=function() return gui.invert_color(COLOR_WHITE, self.is_selected_fn()) end}, }, }, widgets.Label{ view_id='item_filter', frame={t=0, l=28}, text={ {text=self:callback('get_filter_text'), pen=function() return gui.invert_color(COLOR_LIGHTCYAN, self.is_selected_fn()) end}, }, auto_width=true, on_click=function() self.on_filter(self.idx) end, }, widgets.Label{ frame={t=0, l=42}, text='[clear]', text_pen=COLOR_LIGHTRED, auto_width=true, visible=self:callback('has_filter'), on_click=function() self.on_clear_filter(self.idx) end, }, widgets.Label{ frame={t=1, l=2}, text={ {gap=2, 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 and self:getMousePos() then self.on_select(self.idx) end return ItemLine.super.onInput(self, keys) 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()) local buildingplan = require('plugins.buildingplan') self.desc = self.desc or buildingplan.get_desc(filter) self.available = self.available or buildingplan.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' elseif self.available >= 0 then self.note_pen = COLOR_BROWN self.note = (' Will link next (need to make %d)'):format(quantity - self.available) else self.note_pen = COLOR_BROWN self.note = (' Will link later (need to make %d)'):format(-self.available + quantity) end self.note = string.char(192) .. self.note -- character 192 is "└" return ('%d %s%s'):format(quantity, self.desc, quantity == 1 and '' or 's') end function ItemLine:has_filter() return require('plugins.buildingplan').hasFilter( uibs.building_type, uibs.building_subtype, uibs.custom_type, self.idx-1) end function ItemLine:get_filter_text() -- TODO: make this show the filter's materials instead of "edit filters" return self:has_filter() and '[edit filters]' or '[any material]' 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 = 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=22}, } function PlannerOverlay:init() self.selected = 1 self.state = ensure_key(config.data, 'planner') local main_panel = widgets.Panel{ view_id='main', frame={t=1, l=0, r=0, h=14}, frame_style=gui.INTERIOR_MEDIUM_FRAME, frame_background=gui.CLEAR_PEN, visible=self:callback('is_not_minimized'), } local minimized_panel = widgets.Panel{ frame={t=0, r=1, w=17, h=1}, subviews={ widgets.Label{ frame={t=0, r=0, h=1}, text={ {text=' show Planner ', pen=pens.MINI_TEXT_PEN, hpen=pens.MINI_TEXT_HPEN}, {text='['..string.char(31)..']', pen=pens.MINI_BUTT_PEN, hpen=pens.MINI_BUTT_HPEN}, }, visible=self:callback('is_minimized'), on_click=self:callback('toggle_minimized'), }, widgets.Label{ frame={t=0, r=0, h=1}, text={ {text=' hide Planner ', pen=pens.MINI_TEXT_PEN, hpen=pens.MINI_TEXT_HPEN}, {text='['..string.char(30)..']', pen=pens.MINI_BUTT_PEN, hpen=pens.MINI_BUTT_HPEN}, }, visible=self:callback('is_not_minimized'), on_click=self:callback('toggle_minimized'), }, }, } 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 local buildingplan = require('plugins.buildingplan') 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={b=4, l=1, w=21}, key='CUSTOM_H', label='Hollow area:', visible=is_construction, options={ {label='No', value=false}, {label='Yes', value=true, pen=COLOR_GREEN}, }, }, widgets.CycleHotkeyLabel{ view_id='stairs_top_subtype', frame={b=5, l=23, w=30}, 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={b=4, l=23, w=30}, 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.CycleHotkeyLabel { -- TODO: this thing also needs a slider view_id='weapons', frame={b=4, l=1, w=28}, key='CUSTOM_T', key_back='CUSTOM_SHIFT_T', label='Number of weapons:', visible=is_weapon_or_spike_trap, options={ {label='(1)', value=1, pen=COLOR_YELLOW}, {label='(2)', value=2, pen=COLOR_YELLOW}, {label='(3)', value=3, pen=COLOR_YELLOW}, {label='(4)', value=4, pen=COLOR_YELLOW}, {label='(5)', value=5, pen=COLOR_YELLOW}, {label='(6)', value=6, pen=COLOR_YELLOW}, {label='(7)', value=7, pen=COLOR_YELLOW}, {label='(8)', value=8, pen=COLOR_YELLOW}, {label='(9)', value=9, pen=COLOR_YELLOW}, {label='(10)', value=10, pen=COLOR_YELLOW}, }, on_change=function(val) weapon_quantity = val end, }, widgets.ToggleHotkeyLabel { view_id='engraved', frame={b=4, l=1, w=22}, key='CUSTOM_T', label='Engraved only:', visible=is_slab, on_change=function(val) buildingplan.setSpecial(uibs.building_type, uibs.building_subtype, uibs.custom_type, 'engraved', val) end, }, widgets.ToggleHotkeyLabel { view_id='empty', frame={b=4, l=1, w=22}, key='CUSTOM_T', label='Empty only:', visible=is_cage, on_change=function(val) buildingplan.setSpecial(uibs.building_type, uibs.building_subtype, uibs.custom_type, 'empty', val) end, }, widgets.Label{ frame={b=4, l=23}, text_pen=COLOR_DARKGREY, 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=1, w=22}, key='CUSTOM_F', label=function() return buildingplan.hasFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, self.selected - 1) and 'Edit filter' or 'Set filter' end, on_activate=function() self:set_filter(self.selected) end, }, widgets.HotkeyLabel{ frame={b=0, l=1, w=22}, key='CUSTOM_X', label='Clear filter', on_activate=function() self:clear_filter(self.selected) end, enabled=function() return buildingplan.hasFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, self.selected - 1) end }, widgets.CycleHotkeyLabel{ view_id='choose', frame={b=0, l=23}, key='CUSTOM_Z', label='Choose items:', label_below=true, options={ {label='With filters', value=0}, { label=function() local automaterial = itemselection.get_automaterial_selection(uibs.building_type) return ('Last used (%s)'):format(automaterial or 'pick manually') end, value=2, }, {label='Manually', value=1}, }, initial_option=0, on_change=function(choose) buildingplan.setChooseItems(uibs.building_type, uibs.building_subtype, uibs.custom_type, choose) end, }, widgets.CycleHotkeyLabel{ view_id='safety', frame={b=2, l=23, 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) buildingplan.setHeatSafetyFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, heat) end, }, }, }, } local divider_widget = widgets.Panel{ view_id='divider', frame={t=10, l=0, r=0, h=1}, on_render=self:callback('draw_divider_h'), visible=self:callback('is_not_minimized'), } local error_panel = widgets.ResizingPanel{ view_id='errors', frame={t=15, l=0, r=0}, frame_style=gui.BOLD_FRAME, frame_background=gui.CLEAR_PEN, visible=self:callback('is_not_minimized'), } error_panel:addviews{ widgets.WrappedLabel{ frame={t=0, l=1, 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=1, r=0}, text_pen=COLOR_GREEN, text='OK to build', visible=function() return #uibs.errors == 0 end, }, } local prev_next_selector = widgets.Panel{ frame={h=1}, auto_width=true, subviews={ widgets.HotkeyLabel{ frame={t=0, l=1, w=9}, key='CUSTOM_SHIFT_Q', key_sep='\0', label=': Prev/', on_activate=function() self.selected = ((self.selected - 2) % #get_cur_filters()) + 1 end, }, widgets.HotkeyLabel{ frame={t=0, l=2, w=1}, key='CUSTOM_Q', on_activate=function() self.selected = (self.selected % #get_cur_filters()) + 1 end, }, widgets.Label{ frame={t=0,l=10}, text='next item', on_click=function() self.selected = (self.selected % #get_cur_filters()) + 1 end, }, }, visible=function() return #get_cur_filters() > 1 end, } local black_bar = widgets.Panel{ frame={t=0, l=1, w=37, h=1}, frame_inset=0, frame_background=gui.CLEAR_PEN, visible=self:callback('is_not_minimized'), subviews={ prev_next_selector, }, } self:addviews{ black_bar, minimized_panel, main_panel, divider_widget, error_panel, } end function PlannerOverlay:is_minimized() return self.state.minimized end function PlannerOverlay:is_not_minimized() return not self.state.minimized end function PlannerOverlay:toggle_minimized() self.state.minimized = not self.state.minimized config:write() self:reset() end function PlannerOverlay:draw_divider_h(dc) local x2 = dc.width -1 for x=0,x2 do dc:seek(x, 0) if x == 0 then dc:char(nil, pens.HORI_LEFT_PEN) elseif x == x2 then dc:char(nil, pens.HORI_RIGHT_PEN) else dc:char(nil, pens.HORI_MID_PEN) end end 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) filterselection.FilterSelectionScreen{index=idx, desc=require('plugins.buildingplan').get_desc(get_cur_filters()[idx])}:show() end function PlannerOverlay:clear_filter(idx) desc=require('plugins.buildingplan').clearFilter(uibs.building_type, uibs.building_subtype, uibs.custom_type, idx-1) end local function get_placement_data() local direction = uibs.direction local bounds = get_selected_bounds() local width, height, depth = get_cur_area_dims(bounds) 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 start_pos = bounds and xyz2pos(bounds.x1, bounds.y1, bounds.z1) or xyz2pos( uibs.pos.x - adjusted_width//2, uibs.pos.y - adjusted_height//2, uibs.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, uibs.pos.z) end return { x1=min_x, y1=min_y, z1=min_z, x2=max_x, y2=max_y, z2=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 local sp = self.saved_placement self.saved_selection_pos = xyz2pos(sp.x1, sp.y1, sp.z1) self.saved_pos = xyz2pos(sp.x2, sp.y2, sp.z2) self.saved_pos.x = self.saved_pos.x + sp.width - 1 self.saved_pos.y = self.saved_pos.y + sp.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 then if uibs.selection_pos:isValid() then uibs.selection_pos:clear() return true end self.selected = 1 self.subviews.hollow:setOption(false) self:reset() reset_counts_flag = true return false end if keys.CUSTOM_ALT_M then self:toggle_minimized() return true end if PlannerOverlay.super.onInput(self, keys) then return true end if self:is_minimized() then return false end if keys._MOUSE_L 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 local choose = self.subviews.choose:getOptionValue() if choose == 0 then self:place_building(get_placement_data()) else local bounds = get_selected_bounds() self:save_placement() local autoselect = choose == 2 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] = {} local filter = filters[idx] local selection_screen = itemselection.ItemSelectionScreen{ index=idx, desc=require('plugins.buildingplan').get_desc(filter), quantity=get_quantity(filter, is_hollow, bounds), autoselect=autoselect, on_submit=function(items) chosen_items[idx] = items if active_screens[idx] then active_screens[idx]:dismiss() active_screens[idx] = nil else active_screens[idx] = true end 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 _,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, } if active_screens[idx] then -- we've already returned via autoselect active_screens[idx] = nil else active_screens[idx] = selection_screen:show() end end end return true elseif not is_choosing_area() then return false end end end return keys._MOUSE_L_DOWN 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 ONE_BY_ONE = xy2pos(1, 1) function PlannerOverlay:onRenderFrame(dc, rect) PlannerOverlay.super.onRenderFrame(self, dc, rect) if reset_counts_flag then self:reset() local buildingplan = require('plugins.buildingplan') self.subviews.engraved:setOption(buildingplan.getSpecials( uibs.building_type, uibs.building_subtype, uibs.custom_type).engraved or false) self.subviews.empty:setOption(buildingplan.getSpecials( uibs.building_type, uibs.building_subtype, uibs.custom_type).empty or false) self.subviews.choose:setOption(buildingplan.getChooseItems( uibs.building_type, uibs.building_subtype, uibs.custom_type)) self.subviews.safety:setOption(buildingplan.getHeatSafetyFilter( uibs.building_type, uibs.building_subtype, uibs.custom_type)) end if self:is_minimized() then return end local bounds = get_selected_bounds(self.saved_selection_pos, self.saved_pos) if not bounds then return end local hollow = self.subviews.hollow:getOptionValue() local default_pen = (self.saved_selection_pos or #uibs.errors == 0) and pens.GOOD_TILE_PEN or pens.BAD_TILE_PEN local get_pen_fn = is_construction() and function(pos) return dfhack.buildings.checkFreeTiles(pos, ONE_BY_ONE) and pens.GOOD_TILE_PEN or pens.BAD_TILE_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, bounds) local subtype = uibs.building_subtype if pos.z == bounds.z1 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 and shape ~= df.tiletype_shape.STAIR_UPDOWN then subtype = df.construction_type.UpStair end else subtype = opt end elseif pos.z == bounds.z2 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 and shape ~= df.tiletype_shape.STAIR_UPDOWN then subtype = df.construction_type.DownStair end else subtype = opt end end return subtype end function PlannerOverlay:place_building(placement_data, chosen_items) local pd = placement_data local blds = {} local hollow = self.subviews.hollow:getOptionValue() local subtype = uibs.building_subtype local filters = get_cur_filters() if is_pressure_plate() or is_spike_trap() then filters[1].quantity = get_quantity(filters[1]) elseif is_weapon_trap() then filters[2].quantity = get_quantity(filters[2]) end for z=pd.z1,pd.z2 do for y=pd.y1,pd.y2 do for x=pd.x1,pd.x2 do if hollow and x ~= pd.x1 and x ~= pd.x2 and y ~= pd.y1 and y ~= pd.y2 then goto continue end local pos = xyz2pos(x, y, z) if is_stairs() then subtype = self:get_stairs_subtype(pos, pd) end local bld, err = dfhack.buildings.constructBuilding{pos=pos, type=uibs.building_type, subtype=subtype, custom=uibs.custom_type, width=pd.width, height=pd.height, direction=uibs.direction, filters=filters} 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 if k == 'plate_info' then utils.assign(bld.plate_info, uibs.plate_info) 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) local buildingplan = require('plugins.buildingplan') 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 local num_filters = #get_cur_filters() for idx=1,num_filters do local item_ids = chosen_items[idx] local jitem = jitems[num_filters-idx] while jitem.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 jitem.quantity = jitem.quantity - 1 item_ids[#item_ids] = nil end end end buildingplan.addPlannedBuilding(bld) end buildingplan.scheduleCycle() uibs.selection_pos:clear() end return _ENV