implement selecting specific items

develop
Myk Taylor 2023-02-18 01:09:54 -08:00
parent 66a14ecc74
commit 4001ef3815
No known key found for this signature in database
2 changed files with 282 additions and 50 deletions

@ -476,8 +476,8 @@ static void scheduleCycle(color_ostream &out) {
cycle_requested = true; cycle_requested = true;
} }
static int countAvailableItems(color_ostream &out, df::building_type type, int16_t subtype, int32_t custom, int index) { static int scanAvailableItems(color_ostream &out, df::building_type type, int16_t subtype,
DEBUG(status,out).print("entering countAvailableItems\n"); int32_t custom, int index, vector<int> *item_ids = NULL) {
DEBUG(status,out).print( DEBUG(status,out).print(
"entering countAvailableItems building_type=%d subtype=%d custom=%d index=%d\n", "entering countAvailableItems building_type=%d subtype=%d custom=%d index=%d\n",
type, subtype, custom, index); type, subtype, custom, index);
@ -514,15 +514,42 @@ static int countAvailableItems(color_ostream &out, df::building_type type, int16
for (auto vector_id : vector_ids) { for (auto vector_id : vector_ids) {
auto other_id = ENUM_ATTR(job_item_vector_id, other, vector_id); auto other_id = ENUM_ATTR(job_item_vector_id, other, vector_id);
for (auto &item : df::global::world->items.other[other_id]) { for (auto &item : df::global::world->items.other[other_id]) {
if (itemPassesScreen(item) && matchesFilters(item, jitem)) if (itemPassesScreen(item) && matchesFilters(item, jitem)) {
if (item_ids)
item_ids->emplace_back(item->id);
++count; ++count;
} }
} }
}
DEBUG(status,out).print("found matches %d\n", count); DEBUG(status,out).print("found matches %d\n", count);
return count; return count;
} }
static int getAvailableItems(lua_State *L) {
color_ostream *out = Lua::GetOutput(L);
if (!out)
out = &Core::getInstance().getConsole();
df::building_type type = (df::building_type)luaL_checkint(L, 1);
int16_t subtype = luaL_checkint(L, 2);
int32_t custom = luaL_checkint(L, 3);
int index = luaL_checkint(L, 4);
DEBUG(status,*out).print(
"entering getAvailableItems building_type=%d subtype=%d custom=%d index=%d\n",
type, subtype, custom, index);
vector<int> item_ids;
scanAvailableItems(*out, type, subtype, custom, index, &item_ids);
Lua::PushVector(L, item_ids);
return 1;
}
static int countAvailableItems(color_ostream &out, df::building_type type, int16_t subtype, int32_t custom, int index) {
DEBUG(status,out).print(
"entering countAvailableItems building_type=%d subtype=%d custom=%d index=%d\n",
type, subtype, custom, index);
return scanAvailableItems(out, type, subtype, custom, index);
}
static bool hasFilter(color_ostream &out, df::building_type type, int16_t subtype, int32_t custom, int index) { static bool hasFilter(color_ostream &out, df::building_type type, int16_t subtype, int32_t custom, int index) {
DEBUG(status,out).print("entering hasFilter\n"); DEBUG(status,out).print("entering hasFilter\n");
return false; return false;
@ -640,3 +667,8 @@ DFHACK_PLUGIN_LUA_FUNCTIONS {
DFHACK_LUA_FUNCTION(makeTopPriority), DFHACK_LUA_FUNCTION(makeTopPriority),
DFHACK_LUA_END DFHACK_LUA_END
}; };
DFHACK_PLUGIN_LUA_COMMANDS {
DFHACK_LUA_COMMAND(getAvailableItems),
DFHACK_LUA_END
};

@ -20,6 +20,8 @@ local utils = require('utils')
local widgets = require('gui.widgets') local widgets = require('gui.widgets')
require('dfhack.buildings') require('dfhack.buildings')
local uibs = df.global.buildreq
local function process_args(opts, args) local function process_args(opts, args)
if args[1] == 'help' then if args[1] == 'help' then
opts.help = true opts.help = true
@ -64,12 +66,40 @@ function get_job_item(btype, subtype, custom, index)
return obj return obj
end end
local BUTTON_START_PEN, BUTTON_END_PEN = nil, nil 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()
if not is_choosing_area() then return 1, 1, 1 end
return math.abs(uibs.selection_pos.x - uibs.pos.x) + 1,
math.abs(uibs.selection_pos.y - uibs.pos.y) + 1,
math.abs(uibs.selection_pos.z - uibs.pos.z) + 1
end
local function get_quantity(filter)
local quantity = filter.quantity or 1
local dimx, dimy, dimz = get_cur_area_dims()
if quantity < 1 then
quantity = (((dimx * dimy) // 4) + 1) * dimz
else
quantity = quantity * dimx * dimy * dimz
end
return quantity
end
local BUTTON_START_PEN, BUTTON_END_PEN, SELECTED_ITEM_PEN = nil, nil, nil
local reset_counts_flag = false local reset_counts_flag = false
local reset_inspector_flag = false local reset_inspector_flag = false
function signal_reset() function signal_reset()
BUTTON_START_PEN = nil BUTTON_START_PEN = nil
BUTTON_END_PEN = nil BUTTON_END_PEN = nil
SELECTED_ITEM_PEN = nil
reset_counts_flag = true reset_counts_flag = true
reset_inspector_flag = true reset_inspector_flag = true
end end
@ -91,12 +121,169 @@ local function get_button_end_pen()
end end
return BUTTON_END_PEN return BUTTON_END_PEN
end end
local function get_selected_item_pen()
if not SELECTED_ITEM_PEN then
local texpos_base = dfhack.textures.getControlPanelTexposStart()
SELECTED_ITEM_PEN = to_pen{ch='x', fg=COLOR_GREEN,
tile=texpos_base > 0 and texpos_base + 9 or nil}
end
return SELECTED_ITEM_PEN
end
-------------------------------- --------------------------------
-- PlannerOverlay -- ItemSelection
-- --
local uibs = df.global.buildreq ItemSelection = defclass(ItemSelection, widgets.Window)
ItemSelection.ATTRS{
frame_title='Choose items',
frame={w=60, h=30, l=4, t=8},
resizable=true,
resize_min={w=56, h=20},
index=DEFAULT_NIL,
selected_set=DEFAULT_NIL,
}
function ItemSelection:init()
local filter = get_cur_filters()[self.index]
self.quantity = get_quantity(filter)
self.num_selected = 0
self:addviews{
widgets.Label{
frame={t=0},
text={
get_desc(filter),
self.quantity == 1 and '' or 's',
NEWLINE,
('Select up to %d items ('):format(self.quantity),
{text=function() return self.num_selected end},
' selected)',
},
},
widgets.FilteredList{
frame={t=3, l=0, r=0, b=0},
case_sensitive=false,
choices=self:get_choices(),
icon_width=2,
on_submit=self:callback('toggle_item'),
},
}
end
local function make_search_key(str)
local out = ''
for c in str:gmatch("[%w%s]") do
out = out .. c
end
return out
end
function ItemSelection:get_choices()
local item_ids = getAvailableItems(uibs.building_type,
uibs.building_subtype, uibs.custom_type, self.index - 1)
local buckets, selected_buckets = {}, {}
for _,item_id in ipairs(item_ids) do
local item = df.item.find(item_id)
if not item then goto continue end
local desc = dfhack.items.getDescription(item, 0, true)
if buckets[desc] then
local bucket = buckets[desc]
table.insert(bucket.item_ids, item_id)
bucket.quantity = bucket.quantity + 1
else
local entry = {
text=desc,
search_key=make_search_key(desc),
icon=self:callback('get_entry_icon', item_id),
item_ids={item_id},
item_type=item:getType(),
item_subtype=item:getSubtype(),
quantity=1,
selected=false,
}
buckets[desc] = entry
end
::continue::
end
local selected_qty = 0
for bucket in pairs(selected_buckets) do
for _,item_id in ipairs(bucket.item_ids) do
self.selected_set[item_id] = true
end
selected_qty = selected_qty + bucket.quantity
bucket.selected = true
if selected_qty >= self.quantity then break end
end
self.num_selected = selected_qty
local choices = {}
for _,choice in pairs(buckets) do
choice.text = ('(%d) %s'):format(choice.quantity, choice.text)
table.insert(choices, choice)
end
local function choice_sort(a, b)
return a.item_type < b.item_type or
(a.item_type == b.item_type and a.item_subtype < b.item_subtype) or
(a.item_type == b.item_type and a.item_subtype == b.item_subtype and a.search_key < b.search_key)
end
table.sort(choices, choice_sort)
return choices
end
function ItemSelection:toggle_item(_, choice)
if choice.selected then
for _,item_id in ipairs(choice.item_ids) do
self.selected_set[item_id] = nil
end
self.num_selected = self.num_selected - choice.quantity
choice.selected = false
elseif self.quantity > self.num_selected then
for _,item_id in ipairs(choice.item_ids) do
self.selected_set[item_id] = true
end
self.num_selected = self.num_selected + choice.quantity
choice.selected = true
end
end
function ItemSelection:get_entry_icon(item_id)
return self.selected_set[item_id] and get_selected_item_pen() or nil
end
ItemSelectionScreen = defclass(ItemSelectionScreen, gui.ZScreen)
ItemSelectionScreen.ATTRS {
focus_path='buildingplan/itemselection',
force_pause=true,
pass_pause=false,
pass_movement_keys=true,
pass_mouse_clicks=false,
defocusable=false,
index=DEFAULT_NIL,
on_submit=DEFAULT_NIL,
}
function ItemSelectionScreen:init()
self.selected_set = {}
self:addviews{
ItemSelection{
index=self.index,
selected_set=self.selected_set,
}
}
end
function ItemSelectionScreen:onDismiss()
local selected_items = {}
for item_id in pairs(self.selected_set) do
table.insert(selected_items, item_id)
end
self.on_submit(selected_items)
end
--------------------------------
-- PlannerOverlay
--
local function cur_building_has_no_area() local function cur_building_has_no_area()
if uibs.building_type == df.building_type.Construction then return false end if uibs.building_type == df.building_type.Construction then return false end
@ -107,22 +294,6 @@ local function cur_building_has_no_area()
return filters and filters[1] and (not filters[1].quantity or filters[1].quantity > 0) return filters and filters[1] and (not filters[1].quantity or filters[1].quantity > 0)
end end
local function is_choosing_area()
return uibs.selection_pos.x >= 0
end
local function get_cur_area_dims()
if not is_choosing_area() then return 1, 1, 1 end
return math.abs(uibs.selection_pos.x - uibs.pos.x) + 1,
math.abs(uibs.selection_pos.y - uibs.pos.y) + 1,
math.abs(uibs.selection_pos.z - uibs.pos.z) + 1
end
local function get_cur_filters()
return dfhack.buildings.getFiltersByType({}, uibs.building_type,
uibs.building_subtype, uibs.custom_type)
end
local function is_plannable() local function is_plannable()
return get_cur_filters() and return get_cur_filters() and
not (uibs.building_type == df.building_type.Construction not (uibs.building_type == df.building_type.Construction
@ -276,17 +447,6 @@ function get_desc(filter)
return desc return desc
end end
local function get_quantity(filter)
local quantity = filter.quantity or 1
local dimx, dimy, dimz = get_cur_area_dims()
if quantity < 1 then
quantity = (((dimx * dimy) // 4) + 1) * dimz
else
quantity = quantity * dimx * dimy * dimz
end
return quantity
end
function ItemLine:get_item_line_text() function ItemLine:get_item_line_text()
local idx = self.idx local idx = self.idx
local filter = get_cur_filters()[idx] local filter = get_cur_filters()[idx]
@ -357,19 +517,19 @@ function PlannerOverlay:init()
}, },
ItemLine{view_id='item1', frame={t=0, l=0, r=0}, idx=1, ItemLine{view_id='item1', frame={t=0, l=0, r=0}, idx=1,
is_selected_fn=make_is_selected_fn(1), on_select=on_select_fn, is_selected_fn=make_is_selected_fn(1), on_select=on_select_fn,
on_filter=self:callback('filter'), on_filter=self:callback('set_filter'),
on_clear_filter=self:callback('clear_filter')}, on_clear_filter=self:callback('clear_filter')},
ItemLine{view_id='item2', frame={t=2, l=0, r=0}, idx=2, ItemLine{view_id='item2', frame={t=2, l=0, r=0}, idx=2,
is_selected_fn=make_is_selected_fn(2), on_select=on_select_fn, is_selected_fn=make_is_selected_fn(2), on_select=on_select_fn,
on_filter=self:callback('filter'), on_filter=self:callback('set_filter'),
on_clear_filter=self:callback('clear_filter')}, on_clear_filter=self:callback('clear_filter')},
ItemLine{view_id='item3', frame={t=4, l=0, r=0}, idx=3, ItemLine{view_id='item3', frame={t=4, l=0, r=0}, idx=3,
is_selected_fn=make_is_selected_fn(3), on_select=on_select_fn, is_selected_fn=make_is_selected_fn(3), on_select=on_select_fn,
on_filter=self:callback('filter'), on_filter=self:callback('set_filter'),
on_clear_filter=self:callback('clear_filter')}, on_clear_filter=self:callback('clear_filter')},
ItemLine{view_id='item4', frame={t=6, l=0, r=0}, idx=4, ItemLine{view_id='item4', frame={t=6, l=0, r=0}, idx=4,
is_selected_fn=make_is_selected_fn(4), on_select=on_select_fn, is_selected_fn=make_is_selected_fn(4), on_select=on_select_fn,
on_filter=self:callback('filter'), on_filter=self:callback('set_filter'),
on_clear_filter=self:callback('clear_filter')}, on_clear_filter=self:callback('clear_filter')},
widgets.CycleHotkeyLabel{ widgets.CycleHotkeyLabel{
view_id='stairs_top_subtype', view_id='stairs_top_subtype',
@ -426,7 +586,7 @@ function PlannerOverlay:init()
frame={b=1, l=21}, frame={b=1, l=21},
key='CUSTOM_F', key='CUSTOM_F',
label='Set filter', label='Set filter',
on_activate=function() self:filter(self.selected) end, on_activate=function() self:set_filter(self.selected) end,
}, },
widgets.HotkeyLabel{ widgets.HotkeyLabel{
frame={b=1, l=37}, frame={b=1, l=37},
@ -501,8 +661,8 @@ function PlannerOverlay:reset()
reset_counts_flag = false reset_counts_flag = false
end end
function PlannerOverlay:filter(idx) function PlannerOverlay:set_filter(idx)
print('filter', idx) print('set_filter', idx)
end end
function PlannerOverlay:clear_filter(idx) function PlannerOverlay:clear_filter(idx)
@ -539,11 +699,34 @@ function PlannerOverlay:onInput(keys)
local pos = dfhack.gui.getMousePos() local pos = dfhack.gui.getMousePos()
if pos then if pos then
if is_choosing_area() or cur_building_has_no_area() then if is_choosing_area() or cur_building_has_no_area() then
if #get_cur_filters() == 0 then local num_filters = #get_cur_filters()
if num_filters == 0 then
return false -- we don't add value; let the game place it return false -- we don't add value; let the game place it
end end
local choose = self.subviews.choose
if choose.enabled() and choose:getOptionValue() then
local chosen_items = {}
local pending = num_filters
for idx = num_filters,1,-1 do
chosen_items[idx] = {}
if (self.subviews['item'..idx].available or 0) > 0 then
ItemSelectionScreen{
index=self.selected,
on_submit=function(items)
chosen_items[idx] = items
pending = pending - 1
if pending == 0 then
self:place_building(chosen_items)
end
end,
}:show()
else
pending = pending - 1
end
end
else
self:place_building() self:place_building()
uibs.selection_pos:clear() end
return true return true
elseif not is_choosing_area() then elseif not is_choosing_area() then
return false return false
@ -589,7 +772,7 @@ function PlannerOverlay:onRenderFrame(dc, rect)
guidm.renderMapOverlay(get_overlay_pen, bounds) guidm.renderMapOverlay(get_overlay_pen, bounds)
end end
function PlannerOverlay:place_building() function PlannerOverlay:place_building(chosen_items)
local direction = uibs.direction local direction = uibs.direction
local width, height, depth = get_cur_area_dims() local width, height, depth = get_cur_area_dims()
local _, adjusted_width, adjusted_height = dfhack.buildings.getCorrectSize( local _, adjusted_width, adjusted_height = dfhack.buildings.getCorrectSize(
@ -618,11 +801,6 @@ function PlannerOverlay:place_building()
max_y = min_y + height - 1 max_y = min_y + height - 1
max_z = math.max(uibs.selection_pos.z, uibs.pos.z) max_z = math.max(uibs.selection_pos.z, uibs.pos.z)
end end
if self.subviews.choose:getOptionValue() then
-- TODO
-- open dialog, showing all items (restricted to current filters)
-- select items (doesn't have to be all required items)
end
local blds = {} local blds = {}
local subtype = uibs.building_subtype local subtype = uibs.building_subtype
for z=min_z,max_z do for y=min_y,max_y do for x=min_x,max_x do for z=min_z,max_z do for y=min_y,max_y do for x=min_x,max_x do
@ -681,10 +859,32 @@ function PlannerOverlay:place_building()
self.subviews.item3:reduce_quantity() self.subviews.item3:reduce_quantity()
self.subviews.item4:reduce_quantity() self.subviews.item4:reduce_quantity()
for _,bld in ipairs(blds) do for _,bld in ipairs(blds) do
-- TODO: attach chosen items and reduce job_item quantity -- attach chosen items and reduce job_item quantity
if chosen_items then
local job = bld.jobs[0]
local jitems = job.job_items
for idx=1,#get_cur_filters() do
local item_ids = chosen_items[idx]
while jitems[idx-1].quantity > 0 and #item_ids > 0 do
local item_id = item_ids[#item_ids]
local item = df.item.find(item_id)
if not item then
dfhack.printerr(('item no longer available: %d'):format(item_id))
break
end
if not dfhack.job.attachJobItem(job, item, df.job_item_ref.T_role.Hauled, idx-1, -1) then
dfhack.printerr(('cannot attach item: %d'):format(item_id))
break
end
jitems[idx-1].quantity = jitems[idx-1].quantity - 1
item_ids[#item_ids] = nil
end
end
end
addPlannedBuilding(bld) addPlannedBuilding(bld)
end end
scheduleCycle() scheduleCycle()
uibs.selection_pos:clear()
end end
-------------------------------- --------------------------------