dfhack/plugins/lua/buildingplan.lua

2064 lines
70 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 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
2023-02-18 02:09:54 -07:00
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'),
edit_on_char=function(ch) return ch:match('[%l -]') end,
2023-02-19 22:17:03 -07:00
},
2023-02-21 16:05:06 -07:00
widgets.CycleHotkeyLabel{
2023-02-19 22:17:03 -07:00
frame={l=0, b=2},
key='CUSTOM_SHIFT_R',
2023-02-21 16:05:06 -07:00
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_SHIFT_B',
2023-02-21 16:05:06 -07:00
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
--------------------------------
-- 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
2023-02-19 22:17:03 -07:00
--
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
2023-02-19 22:17:03 -07:00
-- 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)
2023-02-23 02:15:22 -07:00
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{
2023-03-05 23:09:21 -07:00
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,
},
},
},
2023-03-05 23:09:21 -07:00
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'),
},
2023-03-05 23:09:21 -07:00
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,
},
},
},
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={t=1, l=1},
2023-02-23 02:15:22 -07:00
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',
2023-03-05 19:31:03 -07:00
on_activate=self:callback('invert_materials'),
2023-02-23 02:15:22 -07:00
},
widgets.HotkeyLabel{
frame={l=30, t=2},
2023-03-05 19:31:03 -07:00
label='Reset filter',
2023-02-23 02:15:22 -07:00
auto_width=true,
2023-03-05 19:31:03 -07:00
key='CUSTOM_SHIFT_X',
on_activate=self:callback('clear_filter'),
2023-02-23 02:15:22 -07:00
},
},
}
}
2023-03-05 23:09:21 -07:00
-- 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')
2023-02-19 22:17:03 -07:00
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())
2023-03-05 23:09:21 -07:00
local prev_filter = self.subviews.search.text
self.subviews.materials_mats:setChoices(mat_choices)
2023-03-05 23:09:21 -07:00
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
2023-03-05 23:09:21 -07:00
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
2023-03-05 19:31:03 -07:00
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
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 QualityAndMaterialsPage:draw_divider(dc)
2023-02-23 02:15:22 -07:00
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{
2023-02-27 11:16:58 -07:00
view_id='blocks',
frame={l=0},
key='CUSTOM_B',
label='Blocks',
label_width=8,
2023-02-27 11:16:58 -07:00
on_change=self:callback('update_setting', 'blocks'),
},
widgets.ToggleHotkeyLabel{
2023-02-27 11:16:58 -07:00
view_id='logs',
frame={l=0},
key='CUSTOM_L',
label='Logs',
label_width=8,
2023-02-27 11:16:58 -07:00
on_change=self:callback('update_setting', 'logs'),
},
widgets.ToggleHotkeyLabel{
2023-02-27 11:16:58 -07:00
view_id='boulders',
frame={l=0},
key='CUSTOM_O',
label='Boulders',
label_width=8,
2023-02-27 11:16:58 -07:00
on_change=self:callback('update_setting', 'boulders'),
},
widgets.ToggleHotkeyLabel{
2023-02-27 11:16:58 -07:00
view_id='bars',
frame={l=0},
key='CUSTOM_R',
label='Bars',
label_width=8,
2023-02-27 11:16:58 -07:00
on_change=self:callback('update_setting', 'bars'),
},
}
2023-02-27 11:16:58 -07:00
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{
2023-03-05 23:09:45 -07:00
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
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()
-- 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
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
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
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(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)
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)
clearFilter(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
if not is_construction() and #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 or keys.SELECT
2023-02-09 01:13:53 -07:00
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
local ONE_BY_ONE = xy2pos(1, 1)
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.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)),
2023-02-09 01:13:53 -07:00
}
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
2023-02-09 01:13:53 -07:00
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
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
-- it's ok if some buildings fail to build
goto continue
2023-02-09 01:13:53 -07:00
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
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)
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',
visible=false, -- until implemented
2023-02-11 03:21:19 -07:00
},
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
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