diff --git a/changelog.txt b/changelog.txt index f01fe6c4ed..4842dae0c3 100644 --- a/changelog.txt +++ b/changelog.txt @@ -15,6 +15,7 @@ that repo. ## New Scripts +- `gui/workorder-details`: adjusts work orders' input item, material, traits - `warn-stealers`: warn when creatures that may steal your food, drinks, or items become visible ## Fixes diff --git a/gui/workorder-details.lua b/gui/workorder-details.lua new file mode 100644 index 0000000000..b710830590 --- /dev/null +++ b/gui/workorder-details.lua @@ -0,0 +1,190 @@ +-- adjust work orders' input item, material, traits +--[====[ + +gui/workorder-details +===================== +Adjust input items, material, or traits for work orders. Actual +jobs created for it will inherit the details. + +This is the equivalent of `gui/workshop-job` for work orders, +with the additional possibility to set input items' traits. + +It has to be run from a work order's detail screen +(:kbd:`j-m`, select work order, :kbd:`d`). + +For best experience add the following to your ``dfhack*.init``:: + + keybinding add D@workquota_details gui/workorder-details + +]====] + +--[[ +Credit goes to the author of `gui/workshop-job`, it showed +me the way. Also, a huge chunk of code could be reused. +]] + +local utils = require 'utils' +local gui = require 'gui' +local guimat = require 'gui.materials' +local widgets = require 'gui.widgets' +local dlg = require 'gui.dialogs' + +local wsj = reqscript 'gui/workshop-job' + +local JobDetails = defclass(JobDetails, gui.FramedScreen) + +JobDetails.focus_path = 'workorder-details' + +JobDetails.ATTRS { + job = DEFAULT_NIL, + frame_inset = 1, + frame_background = COLOR_BLACK, +} + +function JobDetails:init(args) + self:addviews{ + widgets.Label{ + frame = { l = 0, t = 0 }, + text = { + { text = df.job_type.attrs[self.job.job_type].caption }, NEWLINE, NEWLINE, + ' ', status + } + }, + widgets.Label{ + frame = { l = 0, t = 4 }, + text = { + { key = 'CUSTOM_I', text = ': Input item, ', + enabled = self:callback('canChangeIType'), + on_activate = self:callback('onChangeIType') }, + { key = 'CUSTOM_M', text = ': Material, ', + enabled = self:callback('canChangeMat'), + on_activate = self:callback('onChangeMat') }, + { key = 'CUSTOM_T', text = ': Traits', + enabled = self:callback('canChangeTrait'), + on_activate = self:callback('onChangeTrait') } + } + }, + widgets.List{ + view_id = 'list', + frame = { t = 6, b = 2 }, + row_height = 4, + }, + widgets.Label{ + frame = { l = 0, b = 0 }, + text = { + { key = 'LEAVESCREEN', text = ': Back', + on_activate = self:callback('dismiss') } + } + }, + } + + self:initListChoices() +end + +function JobDetails:onGetSelectedJob() + return self.job +end + +local describe_item_type = wsj.describe_item_type +local is_caste_mat = wsj.is_caste_mat +local describe_material = wsj.describe_material +local list_flags = wsj.list_flags + +local function describe_item_traits(iobj) + local line1 = {} + local reaction = df.reaction.find(iobj.reaction_id) + if reaction and #iobj.contains > 0 then + for _,ri in ipairs(iobj.contains) do + table.insert(line1, 'has '..utils.call_with_string( + reaction.reagents[ri],'getDescription',iobj.reaction_id + )) + end + end + if iobj.metal_ore >= 0 then + local ore = dfhack.matinfo.decode(0, iobj.metal_ore) + if ore then + table.insert(line1, 'ore of '..ore:toString()) + end + end + if iobj.has_material_reaction_product ~= '' then + table.insert(line1, iobj.has_material_reaction_product .. '-producing') + end + if iobj.reaction_class ~= '' then + table.insert(line1, 'reaction class '..iobj.reaction_class) + end + if iobj.has_tool_use >= 0 then + table.insert(line1, 'has use '..df.tool_uses[iobj.has_tool_use]) + end + + list_flags(line1, iobj.flags1) + list_flags(line1, iobj.flags2) + list_flags(line1, iobj.flags3) + + if #line1 == 0 then + table.insert(line1, 'no traits') + end + return table.concat(line1, ', ') +end + +function JobDetails:initListChoices() + if not self.job.items then + self.subviews.list:setChoices({}) + return + end + + local choices = {} + for i,iobj in ipairs(self.job.items) do + local head = 'Item '..(i+1)..' x'..iobj.quantity + if iobj.min_dimension > 0 then + head = head .. '(size '..iobj.min_dimension..')' + end + + table.insert(choices, { + index = i, + iobj = iobj, + text = { + head, NEWLINE, + ' ', { text = curry(describe_item_type, iobj) }, NEWLINE, + ' ', { text = curry(describe_material, iobj) }, NEWLINE, + ' ', { text = curry(describe_item_traits, iobj) }, NEWLINE + } + }) + end + + self.subviews.list:setChoices(choices) +end + +JobDetails.canChangeIType = wsj.JobDetails.canChangeIType +JobDetails.setItemType = wsj.JobDetails.setItemType +JobDetails.onChangeIType = wsj.JobDetails.onChangeIType +JobDetails.canChangeMat = wsj.JobDetails.canChangeMat +JobDetails.setMaterial = wsj.JobDetails.setMaterial +JobDetails.findUnambiguousItem = wsj.JobDetails.findUnambiguousItem +JobDetails.onChangeMat = wsj.JobDetails.onChangeMat + +function JobDetails:onInput(keys) + JobDetails.super.onInput(self, keys) +end + +function JobDetails:canChangeTrait() + local idx, obj = self.subviews.list:getSelected() + return obj ~= nil and not is_caste_mat(obj.iobj) +end + +function JobDetails:onChangeTrait() + local idx, obj = self.subviews.list:getSelected() + guimat.ItemTraitsDialog{ + job_item = obj.iobj, + prompt = 'Please select traits for input '..idx, + none_caption = 'no traits', + }:show() +end + +local scr = dfhack.gui.getCurViewscreen() +if not df.viewscreen_workquota_detailsst:is_instance(scr) then + qerror("This script needs to be run from a work order details screen") +end + +-- by opening the viewscreen_workquota_detailsst the +-- work order's .items array is initialized +JobDetails{ job = scr.order }:show() diff --git a/gui/workshop-job.lua b/gui/workshop-job.lua index e31b089511..406c15ebb0 100644 --- a/gui/workshop-job.lua +++ b/gui/workshop-job.lua @@ -47,6 +47,9 @@ and then try to change the input item type, now it won't let you select *plant*; you have to unset the material first. ]====] + +--@ module = true + local utils = require 'utils' local gui = require 'gui' local guidm = require 'gui.dwarfmode' @@ -323,6 +326,10 @@ function JobDetails:onInput(keys) end end +if dfhack_flags.module then + return +end + if not string.match(dfhack.gui.getCurFocus(), '^dwarfmode/QueryBuilding/Some/Workshop/Job') then qerror("This script requires a workshop job selected in the 'q' mode") end diff --git a/test/gui/workorder-details.lua b/test/gui/workorder-details.lua new file mode 100644 index 0000000000..2a1be80642 --- /dev/null +++ b/test/gui/workorder-details.lua @@ -0,0 +1,152 @@ +-- test -dhack/scripts/devel/tests -tworkorder%-details + +config.mode = 'fortress' + +local gui = require('gui') +local function send_keys(...) + local keys = {...} + for _,key in ipairs(keys) do + gui.simulateInput(dfhack.gui.getCurViewscreen(true), key) + end +end + +local xtest = {} -- use to temporarily disable tests (change `function test.somename` to `function xtest.somename`) +local wait = function(n) + --delay(n or 30) -- enable for debugging the tests +end + +-- handle confirm plugin: we may need to additionally confirm order removal +local confirm = require 'plugins.confirm' +local confirmRemove = function() end +if confirm.isEnabled() then + for _, c in pairs(confirm.get_conf_data()) do + if c.id == 'order-remove' then + if c.enabled then + confirmRemove = function() + wait() + send_keys('CUSTOM_P', 'MANAGER_REMOVE') + end + end + break + end + end +end + +function test.changeOrderDetails() + --[[ this is not needed because of how gui.simulateInput'D_JOBLIST' works + -- verify expected starting state + expect.eq(df.ui_sidebar_mode.Default, df.global.ui.main.mode) + expect.true_(df.viewscreen_dwarfmodest:is_instance(scr)) + --]] + + -- get into the orders screen + send_keys('D_JOBLIST', 'UNITJOB_MANAGER') + expect.true_(df.viewscreen_jobmanagementst:is_instance(dfhack.gui.getCurViewscreen(true)), "We need to be in the jobmanagement/Main screen") + + local ordercount = #df.global.world.manager_orders + + --- create an order + dfhack.run_command [[workorder "{ \"frequency\" : \"OneTime\", \"job\" : \"CutGems\", \"material\" : \"INORGANIC:SLADE\" }"]] + wait() + send_keys('STANDARDSCROLL_UP') -- move cursor to newly created CUT SLADE + wait() + send_keys('MANAGER_DETAILS') + expect.true_(df.viewscreen_workquota_detailsst:is_instance(dfhack.gui.getCurViewscreen(true)), "We need to be in the workquota_details screen") + local job = dfhack.gui.getCurViewscreen(true).order + local item = job.items[0] + + dfhack.run_command 'gui/workorder-details' + --[[ + input item: boulder + material: slade + traits: none + ]] + expect.ne(-1, item.item_type, "Input should not be 'any item'") + expect.ne(-1, item.mat_type, "Material should not be 'any material'") + expect.false_(item.flags2.allow_artifact, "Trait allow_artifact should not be set") + + wait() + send_keys('CUSTOM_I', 'SELECT') -- change input to 'any item' + wait() + send_keys('CUSTOM_M', 'SELECT') -- change material to 'any material' + wait() + send_keys('CUSTOM_T', 'STANDARDSCROLL_DOWN', 'STANDARDSCROLL_DOWN', 'SELECT', 'LEAVESCREEN') -- change traits to 'allow_artifact' + --[[ + input item: any item + material: any material + traits: allow_artifact + ]] + expect.eq(-1, item.item_type, "Input item should change to 'any item'") + expect.eq(-1, item.mat_type, "Material should change to 'any material'") + expect.true_(item.flags2.allow_artifact, "Trait allow_artifact should change to set") + + -- cleanup + wait() + send_keys('LEAVESCREEN', 'LEAVESCREEN', 'MANAGER_REMOVE') + confirmRemove() + expect.eq(ordercount, #df.global.world.manager_orders, "Test order should've been removed") + -- go back to map screen + wait() + send_keys('LEAVESCREEN', 'LEAVESCREEN') +end + +-- where flags is a table of key-boolean +local function any_flag(flags) + for f, v in pairs(flags) do + if v then return f end + end + return false +end + +function test.unsetAllItemTraits() + -- get into the orders screen + send_keys('D_JOBLIST', 'UNITJOB_MANAGER') + expect.true_(df.viewscreen_jobmanagementst:is_instance(dfhack.gui.getCurViewscreen(true)), "We need to be in the jobmanagement/Main screen") + + local ordercount = #df.global.world.manager_orders + + --- create an order + dfhack.run_command [[workorder "{ \"frequency\" : \"OneTime\", \"job\" : \"CutGems\", \"material\" : \"INORGANIC:SLADE\" }"]] + wait() + send_keys('STANDARDSCROLL_UP') -- move cursor to newly created CUT SLADE + wait() + send_keys('MANAGER_DETAILS') + expect.true_(df.viewscreen_workquota_detailsst:is_instance(dfhack.gui.getCurViewscreen(true)), "We need to be in the workquota_details screen") + local job = dfhack.gui.getCurViewscreen(true).order + local item = job.items[0] + + dfhack.run_command 'gui/workorder-details' + + -- manually set some traits + item.flags1.improvable = true + item.flags2.allow_artifact = true + item.flags3.unimproved = true + item.has_tool_use = 0 -- LIQUID_COOKING + item.has_material_reaction_product = 'BAG_ITEM' + item.metal_ore = 0 -- iron + item.reaction_class = 'CALCIUM_CARBONATE' + + wait() + send_keys('CUSTOM_T') + wait() + send_keys('SELECT') -- cursor is at 'no traits' + wait() + send_keys('LEAVESCREEN') + + expect.false_(any_flag(item.flags1), "A flag in item.flags1 is set") + expect.false_(any_flag(item.flags2), "A flag in item.flags2 is set") + expect.false_(any_flag(item.flags3), "A flag in item.flags3 is set") + expect.eq(-1, item.has_tool_use, "Tool use is not reset") + expect.eq('', item.has_material_reaction_product, "Material reaction product is not reset") + expect.eq(-1, item.metal_ore, "Metal ore is not reset") + expect.eq('', item.reaction_class, "Reaction class is not reset") + + -- cleanup + wait() + send_keys('LEAVESCREEN', 'LEAVESCREEN', 'MANAGER_REMOVE') + confirmRemove() + expect.eq(ordercount, #df.global.world.manager_orders, "Test order should've been removed") + -- go back to map screen + wait() + send_keys('LEAVESCREEN', 'LEAVESCREEN') +end