dfhack/scripts/gui/workflow.lua

1100 lines
34 KiB
Lua

-- A GUI front-end for the workflow plugin.
--[[=begin
gui/workflow
============
Bind to a key (the example config uses Alt-W), and activate with a job selected
in a workshop in :kbd:`q` mode.
.. image:: /docs/images/workflow.png
This script provides a simple interface to constraints managed by `workflow`.
When active, it displays a list of all constraints applicable to the
current job, and their current status.
A constraint specifies a certain range to be compared against either individual
*item* or whole *stack* count, an item type and optionally a material. When the
current count is below the lower bound of the range, the job is resumed; if it
is above or equal to the top bound, it will be suspended. Within the range, the
specific constraint has no effect on the job; others may still affect it.
Pressing :kbd:`i` switches the current constraint between counting stacks or items.
Pressing :kbd:`r` lets you input the range directly;
:kbd:`e`, :kbd:`r`, :kbd:`d`, :kbd:`f` adjust the
bounds by 5, 10, or 20 depending on the direction and the :kbd:`i` setting (counting
items and expanding the range each gives a 2x bonus).
Pressing :kbd:`a` produces a list of possible outputs of this job as guessed by
workflow, and lets you create a new constraint by choosing one as template. If you
don't see the choice you want in the list, it likely means you have to adjust
the job material first using `job` ``item-material`` or `gui/workshop-job`,
as described in the `workflow` documentation. In this manner, this feature
can be used for troubleshooting jobs that don't match the right constraints.
.. image:: /docs/images/workflow-new1.png
If you select one of the outputs with :kbd:`Enter`, the matching constraint is simply
added to the list. If you use :kbd:`Shift`:kbd:`Enter`, the interface proceeds to the
next dialog, which allows you to edit the suggested constraint parameters to
suit your need, and set the item count range.
.. image:: /docs/images/workflow-new2.png
Pressing :kbd:`s` (or, with the example config, Alt-W in the :kbd:`z` stocks screen)
opens the overall status screen:
.. image:: /docs/images/workflow-status.png
This screen shows all currently existing workflow constraints, and allows
monitoring and/or changing them from one screen. The constraint list can
be filtered by typing text in the field below.
The color of the stock level number indicates how "healthy" the stock level
is, based on current count and trend. Bright green is very good, green is good,
red is bad, bright red is very bad.
The limit number is also color-coded. Red means that there are currently no
workshops producing that item (i.e. no jobs). If it's yellow, that means the
production has been delayed, possibly due to lack of input materials.
The chart on the right is a plot of the last 14 days (28 half day plots) worth
of stock history for the selected item, with the rightmost point representing
the current stock value. The bright green dashed line is the target
limit (maximum) and the dark green line is that minus the gap (minimum).
=end]]
local utils = require 'utils'
local gui = require 'gui'
local guidm = require 'gui.dwarfmode'
local guimat = require 'gui.materials'
local widgets = require 'gui.widgets'
local dlg = require 'gui.dialogs'
local workflow = require 'plugins.workflow'
function check_enabled(cb)
if workflow.isEnabled() then
return cb()
else
dlg.showYesNoPrompt(
'Enable Plugin',
{ 'The workflow plugin is not enabled currently.', NEWLINE, NEWLINE,
'Press ', { key = 'MENU_CONFIRM' }, ' to enable it.' },
COLOR_YELLOW,
function()
workflow.setEnabled(true)
return cb()
end
)
end
end
function check_repeat(job, cb)
if job.flags['repeat'] then
return cb()
else
dlg.showYesNoPrompt(
'Not Repeat Job',
{ 'Workflow only tracks repeating jobs.', NEWLINE, NEWLINE,
'Press ', { key = 'MENU_CONFIRM' }, ' to make this one repeat.' },
COLOR_YELLOW,
function()
job.flags['repeat'] = true
return cb()
end
)
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 matflags = utils.list_bitfield_flags(iobj.mat_mask)
if #matflags > 0 then
matflags = 'any '..table.concat(matflags, '/')
else
matflags = nil
end
if is_caste_mat(iobj) then
return 'no material'
elseif (iobj.mat_type or -1) >= 0 then
local info = dfhack.matinfo.decode(iobj.mat_type, iobj.mat_index)
local matline
if info then
matline = info:toString()
else
matline = iobj.mat_type..':'..iobj.mat_index
end
return matline, matflags
else
return matflags or 'any material'
end
end
function current_stock(iobj)
if iobj.goal_by_count then
return iobj.cur_count
else
return iobj.cur_amount
end
end
function if_by_count(iobj,bc,ba)
if iobj.goal_by_count then
return bc
else
return ba
end
end
function compute_trend(history,field)
local count = #history
if count == 0 then
return 0
end
local sumX,sumY,sumXY,sumXX = 0,0,0,0
for i,v in ipairs(history) do
sumX = sumX + i
sumY = sumY + v[field]
sumXY = sumXY + i*v[field]
sumXX = sumXX + i*i
end
return (count * sumXY - sumX * sumY) / (count * sumXX - sumX * sumX)
end
------------------------
-- RANGE EDITOR GROUP --
------------------------
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,
keys = {
count = 'CUSTOM_SHIFT_I',
modify = 'CUSTOM_SHIFT_R',
min_dec = 'BUILDING_TRIGGER_MIN_SIZE_DOWN',
min_inc = 'BUILDING_TRIGGER_MIN_SIZE_UP',
max_dec = 'BUILDING_TRIGGER_MAX_SIZE_DOWN',
max_inc = 'BUILDING_TRIGGER_MAX_SIZE_UP',
}
}
function RangeEditor:init(args)
self:setText{
{ key = self.keys.count,
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 = self.keys.modify, text = ': Range',
on_activate = self:callback('onEditRange') },
NEWLINE, ' ',
{ key = self.keys.min_dec,
on_activate = self:callback('onIncRange', 'goal_gap', 2) },
{ key = self.keys.min_inc,
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 = self.keys.max_dec,
on_activate = self:callback('onIncRange', 'goal_value', -1) },
{ key = self.keys.max_inc,
on_activate = self:callback('onIncRange', 'goal_value', 2) },
{ 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 * 2
end
cons[field] = math.max(1, cons[field] + delta*5)
self.save_cb(cons)
end
---------------------------
-- NEW CONSTRAINT DIALOG --
---------------------------
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 { item_type = -1 }
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 = function()
if self:isValid() then return COLOR_LIGHTCYAN else return COLOR_LIGHTRED end
end,
text = function()
if self:isValid() then
return describe_item_type(self.constraint)
else
return 'item not set'
end
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 = 14 },
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 = 16 },
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 = ': ',
enabled = self:callback('isValid'),
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:isValid()
return self.constraint.item_type >= 0 or self.constraint.is_craft
end
function NewConstraint:onChange()
local token = workflow.constraintToToken(self.constraint)
local out
if self:isValid() then
out = workflow.findConstraint(token)
end
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
cons.goal_gap = math.max(1, math.min(cons.goal_gap, cons.goal_value-1))
end
------------------------------
-- CONSTRAINT HISTORY GRAPH --
------------------------------
HistoryGraph = defclass(HistoryGraph, widgets.Widget)
HistoryGraph.ATTRS {
frame_inset = 1,
history_pen = COLOR_CYAN,
}
function HistoryGraph:init(info)
end
function HistoryGraph:setData(history, bars)
self.history = history or {}
self.bars = bars or {}
local maxval = 1
for i,v in ipairs(self.history) do
maxval = math.max(maxval, v)
end
for i,v in ipairs(self.bars) do
maxval = math.max(maxval, v.value)
end
self.max_value = maxval
end
function HistoryGraph:onRenderFrame(dc,rect)
dc:fill(rect.x1,rect.y1,rect.x1,rect.y2,{ch='\xb3', fg=COLOR_BROWN})
dc:fill(rect.x1,rect.y2,rect.x2,rect.y2,{ch='\xc4', fg=COLOR_BROWN})
dc:seek(rect.x1,rect.y1):char('\x1e', COLOR_BROWN)
dc:seek(rect.x1,rect.y2):char('\xc5', COLOR_BROWN)
dc:seek(rect.x2,rect.y2):char('\x10', COLOR_BROWN)
dc:seek(rect.x1,rect.y2-1):char('0', COLOR_BROWN)
end
function HistoryGraph:onRenderBody(dc)
local coeff = (dc.height-1)/self.max_value
for i,v in ipairs(self.bars) do
local y = dc.height-1-math.floor(0.5 + coeff*v.value)
dc:fill(0,y,dc.width-1,y,v.pen or {ch='-', fg=COLOR_GREEN})
end
local xbase = dc.width-1-#self.history
for i,v in ipairs(self.history) do
local x = xbase + i
local y = dc.height-1-math.floor(0.5 + coeff*v)
dc:seek(x,y):char('*', self.history_pen)
end
end
------------------------------
-- GLOBAL CONSTRAINT SCREEN --
------------------------------
ConstraintList = defclass(ConstraintList, gui.FramedScreen)
ConstraintList.focus_path = 'workflow/list'
ConstraintList.ATTRS {
frame_title = 'Workflow Status',
frame_inset = 0,
frame_background = COLOR_BLACK,
frame_style = gui.BOUNDARY_FRAME,
}
function ConstraintList:init(args)
local fwidth_cb = self:cb_getfield('fwidth')
self.fwidth = 20
self.sort_by_severity = false
self:addviews{
widgets.Panel{
frame = { l = 0, r = 31 },
frame_inset = 1,
on_layout = function(body)
self.fwidth = body.width - (12+1+1+7+1+1+1+7)
end,
subviews = {
widgets.Label{
frame = { l = 0, t = 0 },
text_pen = COLOR_CYAN,
text = {
{ text = 'Item', width = 12 }, ' ',
{ text = 'Material etc', width = fwidth_cb }, ' ',
{ text = 'Stock / Limit' },
}
},
widgets.FilteredList{
view_id = 'list',
frame = { t = 2, b = 2 },
edit_below = true,
not_found_label = 'No matching constraints',
edit_pen = COLOR_LIGHTCYAN,
text_pen = { fg = COLOR_GREY, bg = COLOR_BLACK },
cursor_pen = { fg = COLOR_WHITE, bg = COLOR_GREEN },
on_select = self:callback('onSelectConstraint'),
},
widgets.Label{
frame = { b = 0, h = 1 },
text = {
{ key = 'CUSTOM_SHIFT_A', text = ': Add',
on_activate = self:callback('onNewConstraint') }, ', ',
{ key = 'CUSTOM_SHIFT_X', text = ': Delete',
on_activate = self:callback('onDeleteConstraint') }, ', ',
{ key = 'CUSTOM_SHIFT_O', text = ': Severity Order',
on_activate = self:callback('onSwitchSort'),
pen = function()
if self.sort_by_severity then
return COLOR_LIGHTCYAN
else
return COLOR_WHITE
end
end },
}
}
}
},
widgets.Panel{
frame = { w = 30, r = 0, h = 6, t = 0 },
frame_inset = 1,
subviews = {
widgets.Label{
frame = { l = 0, t = 0 },
enabled = self:callback('isAnySelected'),
text = {
{ text = function()
local cur = self:getCurConstraint()
if cur then
return string.format(
'Currently %d (%d in use)',
current_stock(cur),
if_by_count(cur, cur.cur_in_use_count, cur.cur_in_use_amount)
)
else
return 'No constraint selected'
end
end }
}
},
RangeEditor{
frame = { l = 0, t = 2 },
enabled = self:callback('isAnySelected'),
get_cb = self:callback('getCurConstraint'),
save_cb = self:callback('saveConstraint'),
keys = {
count = 'CUSTOM_SHIFT_I',
modify = 'CUSTOM_SHIFT_R',
min_dec = 'SECONDSCROLL_PAGEUP',
min_inc = 'SECONDSCROLL_PAGEDOWN',
max_dec = 'SECONDSCROLL_UP',
max_inc = 'SECONDSCROLL_DOWN',
}
},
}
},
widgets.Widget{
active = false,
frame = { w = 1, r = 30 },
frame_background = gui.BOUNDARY_FRAME.frame_pen,
},
widgets.Widget{
active = false,
frame = { w = 30, r = 0, h = 1, t = 6 },
frame_background = gui.BOUNDARY_FRAME.frame_pen,
},
HistoryGraph{
view_id = 'graph',
frame = { w = 30, r = 0, t = 7, b = 0 },
}
}
self:initListChoices(nil, args.select_token)
end
function stock_trend_color(cons)
local stock = current_stock(cons)
if stock >= cons.goal_value - cons.goal_gap then
return COLOR_LIGHTGREEN, 0
elseif stock <= cons.goal_gap then
return COLOR_LIGHTRED, 4
elseif stock >= cons.goal_value - 2*cons.goal_gap then
return COLOR_GREEN, 1
elseif stock <= 2*cons.goal_gap then
return COLOR_RED, 3
else
local trend = if_by_count(cons, cons.trend_count, cons.trend_amount)
if trend > 0.3 then
return COLOR_GREEN, 1
elseif trend < -0.3 then
return COLOR_RED, 3
else
return COLOR_GREY, 2
end
end
end
function ConstraintList:initListChoices(clist, sel_token)
clist = clist or workflow.listConstraints(nil, true)
local fwidth_cb = self:cb_getfield('fwidth')
local choices = {}
for i,cons in ipairs(clist) do
cons.trend_count = compute_trend(cons.history, 'cur_count')
cons.trend_amount = compute_trend(cons.history, 'cur_amount')
local itemstr = describe_item_type(cons)
local matstr,matflagstr = describe_material(cons)
if matflagstr then
matstr = matflagstr .. ' ' .. matstr
end
if cons.min_quality > 0 or cons.is_local then
local lst = {}
if cons.is_local then
table.insert(lst, 'local')
end
if cons.min_quality > 0 then
table.insert(lst, string.lower(df.item_quality[cons.min_quality]))
end
matstr = matstr .. ' ('..table.concat(lst,',')..')'
end
local goal_color = COLOR_GREY
if #cons.jobs == 0 then
goal_color = COLOR_RED
elseif cons.is_delayed then
goal_color = COLOR_YELLOW
end
table.insert(choices, {
text = {
{ text = itemstr, width = 12, pad_char = ' ' }, ' ',
{ text = matstr, width = fwidth_cb, pad_char = ' ' }, ' ',
{ text = curry(current_stock,cons), width = 7, rjustify = true,
pen = function() return { fg = stock_trend_color(cons) } end },
{ text = curry(if_by_count,cons,'S','I'), gap = 1,
pen = { fg = COLOR_GREY } },
{ text = function() return cons.goal_value end, gap = 1,
pen = { fg = goal_color } }
},
severity = select(2, stock_trend_color(cons)),
search_key = itemstr .. ' | ' .. matstr,
token = cons.token,
obj = cons
})
end
self:setChoices(choices, sel_token)
end
function ConstraintList:isAnySelected()
return self.subviews.list:getSelected() ~= nil
end
function ConstraintList:getCurConstraint()
local selidx,selobj = self.subviews.list:getSelected()
if selobj then return selobj.obj end
end
function ConstraintList:onSwitchSort()
self.sort_by_severity = not self.sort_by_severity
self:setChoices(self.subviews.list:getChoices())
end
function ConstraintList:setChoices(choices, sel_token)
if self.sort_by_severity then
table.sort(choices, function(a,b)
return a.severity > b.severity
or (a.severity == b.severity and
current_stock(a.obj)/a.obj.goal_value < current_stock(b.obj)/b.obj.goal_value)
end)
else
table.sort(choices, function(a,b) return a.search_key < b.search_key end)
end
local selidx = nil
if sel_token then
selidx = utils.linear_index(choices, sel_token, 'token')
end
local list = self.subviews.list
local filter = list:getFilter()
list:setChoices(choices, selidx)
if filter ~= '' then
list:setFilter(filter, selidx)
if selidx and list:getSelected() ~= selidx then
list:setFilter('', selidx)
end
end
end
function ConstraintList:onInput(keys)
if keys.LEAVESCREEN then
self:dismiss()
else
ConstraintList.super.onInput(self, keys)
end
end
function ConstraintList:onNewConstraint()
NewConstraint{
on_submit = self:callback('saveConstraint')
}:show()
end
function ConstraintList:saveConstraint(cons)
local out = workflow.setConstraint(cons.token, cons.goal_by_count, cons.goal_value, cons.goal_gap)
self:initListChoices(nil, out.token)
end
function ConstraintList:onDeleteConstraint()
local cons = self:getCurConstraint()
dlg.showYesNoPrompt(
'Delete Constraint',
'Really delete the current constraint?',
COLOR_YELLOW,
function()
workflow.deleteConstraint(cons.token)
self:initListChoices()
end
)
end
function ConstraintList:onSelectConstraint(idx,item)
local history, bars
if item then
local cons = item.obj
local vfield = if_by_count(cons, 'cur_count', 'cur_amount')
bars = {
{ value = cons.goal_value - cons.goal_gap, pen = {ch='-', fg=COLOR_GREEN} },
{ value = cons.goal_value, pen = {ch='-', fg=COLOR_LIGHTGREEN} },
}
history = {}
for i,v in ipairs(cons.history or {}) do
table.insert(history, v[vfield])
end
table.insert(history, cons[vfield])
end
self.subviews.graph:setData(history, bars)
end
-------------------------------
-- WORKSHOP JOB INFO OVERLAY --
-------------------------------
JobConstraints = defclass(JobConstraints, guidm.MenuOverlay)
JobConstraints.focus_path = 'workflow/job'
JobConstraints.ATTRS {
job = DEFAULT_NIL,
frame_inset = 1,
frame_background = COLOR_BLACK,
}
function JobConstraints:init(args)
self.building = dfhack.job.getHolder(self.job)
self:addviews{
widgets.Label{
frame = { l = 0, t = 0 },
text = {
'Workflow Constraints'
}
},
widgets.List{
view_id = 'list',
frame = { t = 2, b = 6 },
row_height = 4,
scroll_keys = widgets.SECONDSCROLL,
},
RangeEditor{
frame = { l = 0, b = 3 },
enabled = self:callback('isAnySelected'),
get_cb = self:callback('getCurConstraint'),
save_cb = self:callback('saveConstraint'),
},
widgets.Label{
frame = { l = 0, b = 0 },
text = {
{ key = 'CUSTOM_SHIFT_A', text = ': Add limit, ',
on_activate = self:callback('onNewConstraint') },
{ key = 'CUSTOM_SHIFT_X', text = ': Delete',
enabled = self:callback('isAnySelected'),
on_activate = self:callback('onDeleteConstraint') },
NEWLINE, NEWLINE,
{ key = 'LEAVESCREEN', text = ': Back',
on_activate = self:callback('dismiss') },
' ',
{ key = 'CUSTOM_SHIFT_S', text = ': Status',
on_activate = function()
local sel = self:getCurConstraint()
ConstraintList{ select_token = (sel or {}).token }:show()
end }
}
},
}
self:initListChoices(args.clist)
end
function JobConstraints:onGetSelectedBuilding()
return self.building
end
function JobConstraints:onGetSelectedJob()
return self.job
end
function JobConstraints:initListChoices(clist, sel_token)
clist = clist or workflow.listConstraints(self.job)
local choices = {}
for i,cons in ipairs(clist) do
local goal = (cons.goal_value-cons.goal_gap)..'-'..cons.goal_value
local curval
if cons.goal_by_count then
goal = goal .. ' stacks'
curval = cons.cur_count
else
goal = goal .. ' items'
curval = cons.cur_amount
end
local order_pen = COLOR_GREY
if cons.request == 'resume' then
order_pen = COLOR_GREEN
elseif cons.request == 'suspend' then
order_pen = COLOR_BLUE
end
local itemstr = describe_item_type(cons)
if cons.min_quality > 0 or cons.is_local then
local lst = {}
if cons.is_local then
table.insert(lst, 'local')
end
if cons.min_quality > 0 then
table.insert(lst, string.lower(df.item_quality[cons.min_quality]))
end
itemstr = itemstr .. ' ('..table.concat(lst,',')..')'
end
local matstr,matflagstr = describe_material(cons)
table.insert(choices, {
text = {
goal, ' ', { text = '(now '..curval..')', pen = order_pen }, NEWLINE,
' ', itemstr, NEWLINE, ' ', matstr, NEWLINE, ' ', (matflagstr or '')
},
token = cons.token,
obj = cons
})
end
local selidx = nil
if sel_token then
selidx = utils.linear_index(choices, sel_token, 'token')
end
self.subviews.list:setChoices(choices, selidx)
end
function JobConstraints:isAnySelected()
return self.subviews.list:getSelected() ~= nil
end
function JobConstraints:getCurConstraint()
local i,v = self.subviews.list:getSelected()
if v then return v.obj end
end
function JobConstraints:saveConstraint(cons)
local out = workflow.setConstraint(cons.token, cons.goal_by_count, cons.goal_value, cons.goal_gap)
self:initListChoices(nil, out.token)
end
function JobConstraints:onNewConstraint()
local outputs = workflow.listJobOutputs(self.job)
if #outputs == 0 then
dlg.showMessage('Unsupported', 'Workflow cannot guess the outputs of this job.', COLOR_LIGHTRED)
return
end
local variants = workflow.listWeakenedConstraints(outputs)
local choices = {}
for i,cons in ipairs(variants) do
local itemstr = describe_item_type(cons)
local matstr,matflags = describe_material(cons)
if matflags then
matstr = matflags..' '..matstr
end
table.insert(choices, { text = itemstr..' of '..matstr, obj = cons })
end
dlg.ListBox{
frame_title = 'Add limit',
text = 'Select one of the possible outputs:',
text_pen = COLOR_WHITE,
choices = choices,
on_select = function(idx,item)
self:saveConstraint(item.obj)
end,
select2_hint = 'Advanced',
on_select2 = function(idx,item)
NewConstraint{
constraint = item.obj,
on_submit = self:callback('saveConstraint')
}:show()
end,
}:show()
end
function JobConstraints:onDeleteConstraint()
local cons = self:getCurConstraint()
dlg.showYesNoPrompt(
'Delete Constraint',
'Really delete the current constraint?',
COLOR_YELLOW,
function()
workflow.deleteConstraint(cons.token)
self:initListChoices()
end
)
end
function JobConstraints:onInput(keys)
if self:propagateMoveKeys(keys) then
if df.global.world.selected_building ~= self.building then
self:dismiss()
end
else
JobConstraints.super.onInput(self, keys)
end
end
local args = {...}
if args[1] == 'status' then
check_enabled(function() ConstraintList{}:show() end)
else
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
local job = dfhack.gui.getSelectedJob()
check_enabled(function()
check_repeat(job, function()
local clist = workflow.listConstraints(job)
if not clist then
dlg.showMessage('Not Supported', 'This type of job is not supported by workflow.', COLOR_LIGHTRED)
return
end
JobConstraints{ job = job, clist = clist }:show()
end)
end)
end