diff --git a/library/lua/gui/materials.lua b/library/lua/gui/materials.lua index 871c60014..f29f530a5 100644 --- a/library/lua/gui/materials.lua +++ b/library/lua/gui/materials.lua @@ -23,6 +23,7 @@ MaterialDialog.ATTRS{ frame_title = 'Select Material', -- new attrs none_caption = 'none', + hide_none = false, use_inorganic = true, use_creature = true, use_plant = true, @@ -68,7 +69,7 @@ function MaterialDialog:init(info) end function MaterialDialog:getWantedFrameSize(rect) - return math.max(40, #self.prompt), math.min(28, rect.height-8) + return math.max(self.frame_width or 40, #self.prompt), math.min(28, rect.height-8) end function MaterialDialog:onDestroy() @@ -78,9 +79,10 @@ function MaterialDialog:onDestroy() end function MaterialDialog:initBuiltinMode() - local choices = { - { text = self.none_caption, mat_type = -1, mat_index = -1 }, - } + local choices = {} + if not self.hide_none then + table.insert(choices, { text = self.none_caption, mat_type = -1, mat_index = -1 }) + end if self.use_inorganic then table.insert(choices, { @@ -281,9 +283,15 @@ function ItemTypeDialog(args) args.with_filter = true args.icon_width = 2 - local choices = { { - icon = '?', text = args.none_caption or 'none', item_type = -1, item_subtype = -1 - } } + local choices = {} + + if not args.hide_none then + table.insert(choices, { + icon = '?', text = args.none_caption or 'none', + item_type = -1, item_subtype = -1 + }) + end + local filter = args.item_filter for itype = 0,df.item_type._last_item do diff --git a/library/lua/gui/widgets.lua b/library/lua/gui/widgets.lua index ad408a2ea..67090e114 100644 --- a/library/lua/gui/widgets.lua +++ b/library/lua/gui/widgets.lua @@ -488,6 +488,18 @@ function List:onRenderBody(dc) local iend = math.min(#choices, top+self.page_size-1) local iw = self.icon_width + local function paint_icon(icon, obj) + if type(icon) ~= 'string' then + dc:char(nil,icon) + else + if current then + dc:string(icon, obj.icon_pen or self.icon_pen or cur_pen) + else + dc:string(icon, obj.icon_pen or self.icon_pen or cur_dpen) + end + end + end + for i = top,iend do local obj = choices[i] local current = (i == self.selected) @@ -499,30 +511,28 @@ function List:onRenderBody(dc) end local y = (i - top)*self.row_height + local icon = getval(obj.icon) - if iw and obj.icon then - local icon = getval(obj.icon) - if icon then - dc:seek(0, y) - if type(icon) ~= 'string' then - dc:char(nil,icon) - else - if current then - dc:string(icon, obj.icon_pen or self.icon_pen or cur_pen) - else - dc:string(icon, obj.icon_pen or self.icon_pen or cur_dpen) - end - end - end + if iw and icon then + dc:seek(0, y) + paint_icon(icon, obj) end render_text(obj, dc, iw or 0, y, cur_pen, cur_dpen, not current) + local ip = dc.width + if obj.key then local keystr = gui.getKeyDisplay(obj.key) - dc:seek(dc.width-2-#keystr,y):pen(self.text_pen) + ip = ip-2-#keystr + dc:seek(ip,y):pen(self.text_pen) dc:string('('):string(keystr,COLOR_LIGHTGREEN):string(')') end + + if icon and not iw then + dc:seek(ip-1,y) + paint_icon(icon, obj) + end end end diff --git a/plugins/lua/workflow.lua b/plugins/lua/workflow.lua index e3fb7b32e..19ca0a84a 100644 --- a/plugins/lua/workflow.lua +++ b/plugins/lua/workflow.lua @@ -9,6 +9,7 @@ local utils = require 'utils' * isEnabled() * setEnabled(enable) * listConstraints([job]) -> {...} + * findConstraint(token) -> {...} or nil * setConstraint(token[, by_count, goal, gap]) -> {...} * deleteConstraint(token) -> true/false @@ -255,7 +256,7 @@ function constraintToToken(cspec) end local mask_part if cspec.mat_mask then - mask_part = table.concat(utils.list_bitfield_flags(cspec.mat_mask), ',') + mask_part = string.upper(table.concat(utils.list_bitfield_flags(cspec.mat_mask), ',')) end local mat_part if cspec.mat_type and cspec.mat_type >= 0 then @@ -270,8 +271,9 @@ function constraintToToken(cspec) if cspec.is_local then table.insert(qlist, "LOCAL") end - if cspec.quality and cspec.quality > 0 then - table.insert(qlist, df.item_quality[cspec.quality] or error('invalid quality: '..cspec.quality)) + if cspec.min_quality and cspec.min_quality > 0 then + local qn = df.item_quality[cspec.min_quality] or error('invalid quality: '..cspec.min_quality) + table.insert(qlist, qn) end local qpart if #qlist > 0 then diff --git a/plugins/workflow.cpp b/plugins/workflow.cpp index 35f99b301..798024f72 100644 --- a/plugins/workflow.cpp +++ b/plugins/workflow.cpp @@ -446,7 +446,7 @@ static void cleanup_state(color_ostream &out) } static void check_lost_jobs(color_ostream &out, int ticks); -static ItemConstraint *get_constraint(color_ostream &out, const std::string &str, PersistentDataItem *cfg = NULL); +static ItemConstraint *get_constraint(color_ostream &out, const std::string &str, PersistentDataItem *cfg = NULL, bool create = true); static void start_protect(color_ostream &out) { @@ -660,7 +660,7 @@ DFhackCExport command_result plugin_onupdate(color_ostream &out) * ITEM COUNT CONSTRAINT * ******************************/ -static ItemConstraint *get_constraint(color_ostream &out, const std::string &str, PersistentDataItem *cfg) +static ItemConstraint *get_constraint(color_ostream &out, const std::string &str, PersistentDataItem *cfg, bool create) { std::vector tokens; split_string(&tokens, str, "/"); @@ -683,7 +683,7 @@ static ItemConstraint *get_constraint(color_ostream &out, const std::string &str if (item.subtype >= 0) weight += 10000; - df::dfhack_material_category mat_mask; + df::dfhack_material_category mat_mask(0); std::string maskstr = vector_get(tokens,1); if (!maskstr.empty() && !parseJobMaterialCategory(&mat_mask, maskstr)) { out.printerr("Cannot decode material mask: %s\n", maskstr.c_str()); @@ -757,6 +757,9 @@ static ItemConstraint *get_constraint(color_ostream &out, const std::string &str return ct; } + if (!create) + return NULL; + ItemConstraint *nct = new ItemConstraint; nct->is_craft = is_craft; nct->item = item; @@ -1346,7 +1349,9 @@ static void push_constraint(lua_State *L, ItemConstraint *cv) Lua::SetField(L, cv->is_craft, ctable, "is_craft"); + lua_getglobal(L, "copyall"); Lua::PushDFObject(L, &cv->mat_mask); + lua_call(L, 1, 1); lua_setfield(L, -2, "mat_mask"); Lua::SetField(L, cv->material.type, ctable, "mat_type"); @@ -1417,6 +1422,22 @@ static int listConstraints(lua_State *L) return 1; } +static int findConstraint(lua_State *L) +{ + auto token = luaL_checkstring(L, 1); + + color_ostream &out = *Lua::GetOutput(L); + update_data_structures(out); + + ItemConstraint *icv = get_constraint(out, token, NULL, false); + + if (icv) + push_constraint(L, icv); + else + lua_pushnil(L); + return 1; +} + static int setConstraint(lua_State *L) { auto token = luaL_checkstring(L, 1); @@ -1425,6 +1446,7 @@ static int setConstraint(lua_State *L) int gap = luaL_optint(L, 4, -1); color_ostream &out = *Lua::GetOutput(L); + update_data_structures(out); ItemConstraint *icv = get_constraint(out, token); if (!icv) @@ -1451,6 +1473,7 @@ DFHACK_PLUGIN_LUA_FUNCTIONS { DFHACK_PLUGIN_LUA_COMMANDS { DFHACK_LUA_COMMAND(listConstraints), + DFHACK_LUA_COMMAND(findConstraint), DFHACK_LUA_COMMAND(setConstraint), DFHACK_LUA_END }; diff --git a/scripts/gui/workflow.lua b/scripts/gui/workflow.lua index 84540b5ca..4a9e5c913 100644 --- a/scripts/gui/workflow.lua +++ b/scripts/gui/workflow.lua @@ -43,9 +43,357 @@ function check_repeat(job, cb) end end +function describe_item_type(iobj) + local itemline = 'any item' + if iobj.is_craft then + itemline = 'any craft' + elseif iobj.item_type >= 0 then + itemline = df.item_type.attrs[iobj.item_type].caption or iobj.item_type + local subtype = iobj.item_subtype or -1 + local def = dfhack.items.getSubtypeDef(iobj.item_type, subtype) + local count = dfhack.items.getSubtypeCount(iobj.item_type, subtype) + if def then + itemline = def.name + elseif count >= 0 then + itemline = 'any '..itemline + end + end + return itemline +end + +function is_caste_mat(iobj) + return dfhack.items.isCasteMaterial(iobj.item_type or -1) +end + +function describe_material(iobj) + local matline = 'any material' + if is_caste_mat(iobj) then + matline = 'no material' + elseif (iobj.mat_type or -1) >= 0 then + local info = dfhack.matinfo.decode(iobj.mat_type, iobj.mat_index) + if info then + matline = info:toString() + else + matline = iobj.mat_type..':'..iobj.mat_index + end + end + return matline +end + +local null_cons = { goal_value = 0, goal_gap = 0, goal_by_count = false } + +RangeEditor = defclass(RangeEditor, widgets.Label) + +RangeEditor.ATTRS { + get_cb = DEFAULT_NIL, + save_cb = DEFAULT_NIL +} + +function RangeEditor:init(args) + self:setText{ + { key = 'BUILDING_TRIGGER_ENABLE_CREATURE', + text = function() + local cons = self.get_cb() or null_cons + if cons.goal_by_count then + return ': Count stacks ' + else + return ': Count items ' + end + end, + on_activate = self:callback('onChangeUnit') }, + { key = 'BUILDING_TRIGGER_ENABLE_MAGMA', text = ': Modify', + on_activate = self:callback('onEditRange') }, + NEWLINE, ' ', + { key = 'BUILDING_TRIGGER_MIN_SIZE_DOWN', + on_activate = self:callback('onIncRange', 'goal_gap', 5) }, + { key = 'BUILDING_TRIGGER_MIN_SIZE_UP', + on_activate = self:callback('onIncRange', 'goal_gap', -1) }, + { text = function() + local cons = self.get_cb() or null_cons + return string.format(': Min %-4d ', cons.goal_value - cons.goal_gap) + end }, + { key = 'BUILDING_TRIGGER_MAX_SIZE_DOWN', + on_activate = self:callback('onIncRange', 'goal_value', -1) }, + { key = 'BUILDING_TRIGGER_MAX_SIZE_UP', + on_activate = self:callback('onIncRange', 'goal_value', 5) }, + { text = function() + local cons = self.get_cb() or null_cons + return string.format(': Max %-4d', cons.goal_value) + end }, + } +end + +function RangeEditor:onChangeUnit() + local cons = self.get_cb() + cons.goal_by_count = not cons.goal_by_count + self.save_cb(cons) +end + +function RangeEditor:onEditRange() + local cons = self.get_cb() + dlg.showInputPrompt( + 'Input Range', + 'Enter the new constraint range:', + COLOR_WHITE, + (cons.goal_value-cons.goal_gap)..'-'..cons.goal_value, + function(text) + local maxv = string.match(text, '^%s*(%d+)%s*$') + if maxv then + cons.goal_value = maxv + return self.save_cb(cons) + end + local minv,maxv = string.match(text, '^%s*(%d+)-(%d+)%s*$') + if minv and maxv and minv ~= maxv then + cons.goal_value = math.max(minv,maxv) + cons.goal_gap = math.abs(maxv-minv) + return self.save_cb(cons) + end + dlg.showMessage('Invalid Range', 'This range is invalid: '..text, COLOR_LIGHTRED) + end + ) +end + +function RangeEditor:onIncRange(field, delta) + local cons = self.get_cb() + if not cons.goal_by_count then + delta = delta * 5 + end + cons[field] = math.max(1, cons[field] + delta) + self.save_cb(cons) +end + +NewConstraint = defclass(NewConstraint, gui.FramedScreen) + +NewConstraint.focus_path = 'workflow/new' + +NewConstraint.ATTRS { + frame_style = gui.GREY_LINE_FRAME, + frame_title = 'New workflow constraint', + frame_width = 39, + frame_height = 20, + frame_inset = 1, + constraint = DEFAULT_NIL, + on_submit = DEFAULT_NIL, +} + +function NewConstraint:init(args) + self.constraint = args.constraint or {} + rawset_default(self.constraint, { goal_value = 10, goal_gap = 5, goal_by_count = false }) + + local matlist = {} + local matsel = 1 + local matmask = self.constraint.mat_mask + + for i,v in ipairs(df.dfhack_material_category) do + if v and v ~= 'wood2' then + table.insert(matlist, { icon = self:callback('isMatSelected', v), text = v }) + if matmask and matmask[v] and matsel == 1 then + matsel = #matlist + end + end + end + + self:addviews{ + widgets.Label{ + frame = { l = 0, t = 0 }, + text = 'Items matching:' + }, + widgets.Label{ + frame = { l = 1, t = 2, w = 26 }, + text = { + 'Type: ', + { pen = COLOR_LIGHTCYAN, + text = function() return describe_item_type(self.constraint) end }, + NEWLINE, ' ', + { key = 'CUSTOM_T', text = ': Select, ', + on_activate = self:callback('chooseType') }, + { key = 'CUSTOM_SHIFT_C', text = ': Crafts', + on_activate = self:callback('chooseCrafts') }, + NEWLINE, NEWLINE, + 'Material: ', + { pen = COLOR_LIGHTCYAN, + text = function() return describe_material(self.constraint) end }, + NEWLINE, ' ', + { key = 'CUSTOM_P', text = ': Specific', + on_activate = self:callback('chooseMaterial') }, + NEWLINE, NEWLINE, + 'Other:', + NEWLINE, ' ', + { key = 'D_MILITARY_SUPPLIES_WATER_DOWN', + on_activate = self:callback('incQuality', -1) }, + { key = 'D_MILITARY_SUPPLIES_WATER_UP', key_sep = ': ', + text = function() + return df.item_quality[self.constraint.min_quality or 0]..' quality' + end, + on_activate = self:callback('incQuality', 1) }, + NEWLINE, ' ', + { key = 'CUSTOM_L', key_sep = ': ', + text = function() + if self.constraint.is_local then + return 'Locally made only' + else + return 'Include foreign' + end + end, + on_activate = self:callback('toggleLocal') }, + } + }, + widgets.Label{ + frame = { l = 0, t = 13 }, + text = { + 'Desired range: ', + { pen = COLOR_LIGHTCYAN, + text = function() + local cons = self.constraint + local goal = (cons.goal_value-cons.goal_gap)..'-'..cons.goal_value + if cons.goal_by_count then + return goal .. ' stacks' + else + return goal .. ' items' + end + end }, + } + }, + RangeEditor{ + frame = { l = 1, t = 15 }, + get_cb = self:cb_getfield('constraint'), + save_cb = self:callback('onRangeChange'), + }, + widgets.Label{ + frame = { l = 30, t = 0 }, + text = 'Mat class' + }, + widgets.List{ + view_id = 'matlist', + frame = { l = 30, t = 2, w = 9, h = 18 }, + scroll_keys = widgets.STANDARDSCROLL, + choices = matlist, + selected = matsel, + on_submit = self:callback('onToggleMatclass') + }, + widgets.Label{ + frame = { l = 0, b = 0, w = 29 }, + text = { + { key = 'LEAVESCREEN', text = ': Cancel, ', + on_activate = self:callback('dismiss') }, + { key = 'MENU_CONFIRM', key_sep = ': ', + text = function() + if self.is_existing then return 'Update' else return 'Create new' end + end, + on_activate = function() + self:dismiss() + if self.on_submit then + self.on_submit(self.constraint) + end + end }, + } + }, + } +end + +function NewConstraint:postinit() + self:onChange() +end + +function NewConstraint:onChange() + local token = workflow.constraintToToken(self.constraint) + local out = workflow.findConstraint(token) + + if out then + self.constraint = out + self.is_existing = true + else + self.constraint.token = token + self.is_existing = false + end +end + +function NewConstraint:chooseType() + guimat.ItemTypeDialog{ + prompt = 'Please select a new item type', + hide_none = true, + on_select = function(itype,isub) + local cons = self.constraint + cons.item_type = itype + cons.item_subtype = isub + cons.is_craft = nil + self:onChange() + end + }:show() +end + +function NewConstraint:chooseCrafts() + local cons = self.constraint + cons.item_type = -1 + cons.item_subtype = -1 + cons.is_craft = true + self:onChange() +end + +function NewConstraint:chooseMaterial() + local cons = self.constraint + guimat.MaterialDialog{ + prompt = 'Please select a new material', + none_caption = 'any material', + frame_width = 37, + on_select = function(mat_type, mat_index) + local cons = self.constraint + cons.mat_type = mat_type + cons.mat_index = mat_index + cons.mat_mask = nil + self:onChange() + end + }:show() +end + +function NewConstraint:incQuality(diff) + local cons = self.constraint + local nq = (cons.min_quality or 0) + diff + if nq < 0 then + nq = df.item_quality.Masterful + elseif nq > df.item_quality.Masterful then + nq = 0 + end + cons.min_quality = nq + self:onChange() +end + +function NewConstraint:toggleLocal() + local cons = self.constraint + cons.is_local = not cons.is_local + self:onChange() +end + +function NewConstraint:isMatSelected(token) + if self.constraint.mat_mask and self.constraint.mat_mask[token] then + return { ch = '\xfb', fg = COLOR_LIGHTGREEN } + else + return nil + end +end + +function NewConstraint:onToggleMatclass(idx,obj) + local cons = self.constraint + if cons.mat_mask and cons.mat_mask[obj.text] then + cons.mat_mask[obj.text] = false + else + cons.mat_mask = cons.mat_mask or {} + cons.mat_mask[obj.text] = true + cons.mat_type = -1 + cons.mat_index = -1 + end + self:onChange() +end + +function NewConstraint:onRangeChange() + local cons = self.constraint + local lim = math.max(cons.goal_value-5, math.floor(cons.goal_value/2)) + cons.goal_gap = math.max(1, math.min(cons.goal_gap, lim)) +end + JobConstraints = defclass(JobConstraints, guidm.MenuOverlay) -JobConstraints.focus_path = 'workflow-job' +JobConstraints.focus_path = 'workflow/job' JobConstraints.ATTRS { job = DEFAULT_NIL, @@ -53,8 +401,6 @@ JobConstraints.ATTRS { frame_background = COLOR_BLACK, } -local null_cons = { goal_value = 0, goal_gap = 0, goal_by_count = false } - function JobConstraints:init(args) self.building = dfhack.job.getHolder(self.job) @@ -71,40 +417,11 @@ function JobConstraints:init(args) row_height = 4, scroll_keys = widgets.SECONDSCROLL, }, - widgets.Label{ + RangeEditor{ frame = { l = 0, b = 3 }, enabled = self:callback('isAnySelected'), - text = { - { key = 'BUILDING_TRIGGER_ENABLE_CREATURE', - text = function() - local cons = self:getCurConstraint() or null_cons - if cons.goal_by_count then - return ': Count stacks ' - else - return ': Count items ' - end - end, - on_activate = self:callback('onChangeUnit') }, - { key = 'BUILDING_TRIGGER_ENABLE_MAGMA', text = ': Modify', - on_activate = self:callback('onEditRange') }, - NEWLINE, ' ', - { key = 'BUILDING_TRIGGER_MIN_SIZE_DOWN', - on_activate = self:callback('onIncRange', 'goal_gap', 5) }, - { key = 'BUILDING_TRIGGER_MIN_SIZE_UP', - on_activate = self:callback('onIncRange', 'goal_gap', -1) }, - { text = function() - local cons = self:getCurConstraint() or null_cons - return string.format(': Min %-4d ', cons.goal_value - cons.goal_gap) - end }, - { key = 'BUILDING_TRIGGER_MAX_SIZE_DOWN', - on_activate = self:callback('onIncRange', 'goal_value', -1) }, - { key = 'BUILDING_TRIGGER_MAX_SIZE_UP', - on_activate = self:callback('onIncRange', 'goal_value', 5) }, - { text = function() - local cons = self:getCurConstraint() or null_cons - return string.format(': Max %-4d', cons.goal_value) - end }, - } + get_cb = self:callback('getCurConstraint'), + save_cb = self:callback('saveConstraint'), }, widgets.Label{ frame = { l = 0, b = 0 }, @@ -132,43 +449,6 @@ function JobConstraints:onGetSelectedJob() return self.job end -function describe_item_type(iobj) - local itemline = 'any item' - if iobj.is_craft then - itemline = 'any craft' - elseif iobj.item_type >= 0 then - itemline = df.item_type.attrs[iobj.item_type].caption or iobj.item_type - local subtype = iobj.item_subtype or -1 - local def = dfhack.items.getSubtypeDef(iobj.item_type, subtype) - local count = dfhack.items.getSubtypeCount(iobj.item_type, subtype) - if def then - itemline = def.name - elseif count >= 0 then - itemline = 'any '..itemline - end - end - return itemline -end - -function is_caste_mat(iobj) - return dfhack.items.isCasteMaterial(iobj.item_type or -1) -end - -function describe_material(iobj) - local matline = 'any material' - if is_caste_mat(iobj) then - matline = 'no material' - elseif (iobj.mat_type or -1) >= 0 then - local info = dfhack.matinfo.decode(iobj.mat_type, iobj.mat_index) - if info then - matline = info:toString() - else - matline = iobj.mat_type..':'..iobj.mat_index - end - end - return matline -end - function JobConstraints:initListChoices(clist, sel_token) clist = clist or workflow.listConstraints(self.job) @@ -247,45 +527,6 @@ function JobConstraints:saveConstraint(cons) self:initListChoices(nil, out.token) end -function JobConstraints:onChangeUnit() - local cons = self:getCurConstraint() - cons.goal_by_count = not cons.goal_by_count - self:saveConstraint(cons) -end - -function JobConstraints:onEditRange() - local cons = self:getCurConstraint() - dlg.showInputPrompt( - 'Input Range', - 'Enter the new constraint range:', - COLOR_WHITE, - (cons.goal_value-cons.goal_gap)..'-'..cons.goal_value, - function(text) - local maxv = string.match(text, '^%s*(%d+)%s*$') - if maxv then - cons.goal_value = maxv - return self:saveConstraint(cons) - end - local minv,maxv = string.match(text, '^%s*(%d+)-(%d+)%s*$') - if minv and maxv and minv ~= maxv then - cons.goal_value = math.max(minv,maxv) - cons.goal_gap = math.abs(maxv-minv) - return self:saveConstraint(cons) - end - dlg.showMessage('Invalid Range', 'This range is invalid: '..text, COLOR_LIGHTRED) - end - ) -end - -function JobConstraints:onIncRange(field, delta) - local cons = self:getCurConstraint() - if not cons.goal_by_count then - delta = delta * 5 - end - cons[field] = math.max(1, cons[field] + delta) - self:saveConstraint(cons) -end - function JobConstraints:onNewConstraint() local outputs = workflow.listJobOutputs(self.job) if #outputs == 0 then @@ -318,7 +559,10 @@ function JobConstraints:onNewConstraint() COLOR_WHITE, choices, function(idx,item) - self:saveConstraint(item.obj) + NewConstraint{ + constraint = item.obj, + on_submit = self:callback('saveConstraint') + }:show() end ) end