1100 lines
34 KiB
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
|