Pressing 'c' switches the current constraint between counting stacks or items.
-Pressing 'm' lets you input the range directly; 'e', 'r', 'd', 'f' adjust the
-bounds by 1, 5, or 25 depending on the direction and the 'c' setting (counting
-items and expanding the range each gives a 5x bonus).
-Pressing 'I' switches the current constraint between counting stacks or items.
+Pressing 'R' lets you input the range directly; 'e', 'r', 'd', 'f' adjust the
+bounds by 5, 10, or 20 depending on the direction and the 'I' setting (counting
+items and expanding the range each gives a 2x bonus).
+Pressing '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,
@@ -3050,6 +3050,23 @@ added to the list. If you use Shift-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.
Pressing 'S' (or, with the example config, Alt-W in the 'z' stocks screen)
+opens the overall status screen, which was copied from the C++ implementation
+by falconne for better integration with the rest of the lua script:
+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).
diff --git a/Readme.rst b/Readme.rst
index a214a6ecb..a84691b05 100644
--- a/Readme.rst
+++ b/Readme.rst
@@ -2295,12 +2295,12 @@ 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 'c' switches the current constraint between counting stacks or items.
-Pressing 'm' lets you input the range directly; 'e', 'r', 'd', 'f' adjust the
-bounds by 1, 5, or 25 depending on the direction and the 'c' setting (counting
-items and expanding the range each gives a 5x bonus).
+Pressing 'I' switches the current constraint between counting stacks or items.
+Pressing 'R' lets you input the range directly; 'e', 'r', 'd', 'f' adjust the
+bounds by 5, 10, or 20 depending on the direction and the 'I' setting (counting
+items and expanding the range each gives a 2x bonus).
-Pressing 'n' produces a list of possible outputs of this job as guessed by
+Pressing '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``,
@@ -2316,6 +2316,29 @@ suit your need, and set the item count range.
.. image:: images/workflow-new2.png
+Pressing 'S' (or, with the example config, Alt-W in the 'z' stocks screen)
+opens the overall status screen, which was copied from the C++ implementation
+by falconne for better integration with the rest of the lua script:
+
+.. image:: 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).
+
gui/assign-rack
===============
diff --git a/dfhack.init-example b/dfhack.init-example
index 8fafa4cf4..7617b9f6e 100644
--- a/dfhack.init-example
+++ b/dfhack.init-example
@@ -91,6 +91,7 @@ keybinding add Alt-A@dwarfmode/QueryBuilding/Some/Workshop/Job gui/workshop-job
# workflow front-end
keybinding add Alt-W@dwarfmode/QueryBuilding/Some/Workshop/Job gui/workflow
+keybinding add Alt-W@overallstatus "gui/workflow status"
# assign weapon racks to squads so that they can be used
keybinding add P@dwarfmode/QueryBuilding/Some/Weaponrack gui/assign-rack
diff --git a/images/workflow-new1.png b/images/workflow-new1.png
index 50d0e1f42..498fc4e80 100644
Binary files a/images/workflow-new1.png and b/images/workflow-new1.png differ
diff --git a/images/workflow-new2.png b/images/workflow-new2.png
index 74a4922be..58698d30f 100644
Binary files a/images/workflow-new2.png and b/images/workflow-new2.png differ
diff --git a/images/workflow-status.png b/images/workflow-status.png
new file mode 100644
index 000000000..6c3d989f3
Binary files /dev/null and b/images/workflow-status.png differ
diff --git a/images/workflow.png b/images/workflow.png
index a0a0d4216..7506c730f 100644
Binary files a/images/workflow.png and b/images/workflow.png differ
diff --git a/library/lua/gui/widgets.lua b/library/lua/gui/widgets.lua
index ebbffbbbd..cb9e1c9be 100644
--- a/library/lua/gui/widgets.lua
+++ b/library/lua/gui/widgets.lua
@@ -418,7 +418,13 @@ List.ATTRS{
function List:init(info)
self.page_top = 1
self.page_size = 1
- self:setChoices(info.choices, info.selected)
+
+ if info.choices then
+ self:setChoices(info.choices, info.selected)
+ else
+ self.choices = {}
+ self.selected = 1
+ end
end
function List:setChoices(choices, selected)
@@ -481,6 +487,9 @@ function List:moveCursor(delta, force_cb)
if cnt < 1 then
self.page_top = 1
self.selected = 1
+ if force_cb and self.on_select then
+ self.on_select(nil,nil)
+ end
return
end
@@ -657,13 +666,17 @@ function FilteredList:init(info)
end
end
self.not_found = Label{
- visible = false,
+ visible = true,
text = info.not_found_label or 'No matches',
text_pen = COLOR_LIGHTRED,
frame = { l = info.icon_width, t = self.list.frame.t },
}
self:addviews{ self.edit, self.list, self.not_found }
- self:setChoices(info.choices, info.selected)
+ if info.choices then
+ self:setChoices(info.choices, info.selected)
+ else
+ self.choices = {}
+ end
end
function FilteredList:getChoices()
diff --git a/scripts/gui/workflow.lua b/scripts/gui/workflow.lua
index 9a45f6554..77a87c9ce 100644
--- a/scripts/gui/workflow.lua
+++ b/scripts/gui/workflow.lua
@@ -130,12 +130,20 @@ RangeEditor = defclass(RangeEditor, widgets.Label)
RangeEditor.ATTRS {
get_cb = DEFAULT_NIL,
- save_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 = 'BUILDING_TRIGGER_ENABLE_CREATURE',
+ { key = self.keys.count,
text = function()
local cons = self.get_cb() or null_cons
if cons.goal_by_count then
@@ -145,21 +153,21 @@ function RangeEditor:init(args)
end
end,
on_activate = self:callback('onChangeUnit') },
- { key = 'BUILDING_TRIGGER_ENABLE_MAGMA', text = ': Modify',
+ { key = self.keys.modify, text = ': Range',
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',
+ { 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 = 'BUILDING_TRIGGER_MAX_SIZE_DOWN',
+ { key = self.keys.max_dec,
on_activate = self:callback('onIncRange', 'goal_value', -1) },
- { key = 'BUILDING_TRIGGER_MAX_SIZE_UP',
- on_activate = self:callback('onIncRange', 'goal_value', 5) },
+ { 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)
@@ -200,9 +208,9 @@ end
function RangeEditor:onIncRange(field, delta)
local cons = self.get_cb()
if not cons.goal_by_count then
- delta = delta * 5
+ delta = delta * 2
end
- cons[field] = math.max(1, cons[field] + delta)
+ cons[field] = math.max(1, cons[field] + delta*5)
self.save_cb(cons)
end
@@ -295,7 +303,7 @@ function NewConstraint:init(args)
}
},
widgets.Label{
- frame = { l = 0, t = 13 },
+ frame = { l = 0, t = 14 },
text = {
'Desired range: ',
{ pen = COLOR_LIGHTCYAN,
@@ -311,7 +319,7 @@ function NewConstraint:init(args)
}
},
RangeEditor{
- frame = { l = 1, t = 15 },
+ frame = { l = 1, t = 16 },
get_cb = self:cb_getfield('constraint'),
save_cb = self:callback('onRangeChange'),
},
@@ -353,7 +361,7 @@ function NewConstraint:postinit()
end
function NewConstraint:isValid()
- return self.constraint.item_type >= 0
+ return self.constraint.item_type >= 0 or self.constraint.is_craft
end
function NewConstraint:onChange()
@@ -455,6 +463,59 @@ function NewConstraint:onRangeChange()
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 --
------------------------------
@@ -478,47 +539,7 @@ function ConstraintList:init(args)
self:addviews{
widgets.Panel{
- frame = { w = 31, 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'),
- },
- }
- },
- widgets.Widget{
- active = false,
- frame = { w = 1, r = 31 },
- frame_background = gui.BOUNDARY_FRAME.frame_pen,
- },
- widgets.Widget{
- active = false,
- frame = { w = 31, r = 0, h = 1, t = 6 },
- frame_background = gui.BOUNDARY_FRAME.frame_pen,
- },
- widgets.Panel{
- frame = { l = 0, r = 32 },
+ frame = { l = 0, r = 31 },
frame_inset = 1,
on_layout = function(body)
self.fwidth = body.width - (12+1+1+7+1+1+1+7)
@@ -541,6 +562,7 @@ function ConstraintList:init(args)
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 },
@@ -557,27 +579,66 @@ function ConstraintList:init(args)
else
return COLOR_WHITE
end
- end }, ', ',
- { key = 'CUSTOM_SHIFT_S', text = ': Search',
- on_activate = function()
- self.subviews.list.edit.active = not self.subviews.list.edit.active
- end,
- pen = function()
- if self.subviews.list.edit.active then
- return COLOR_LIGHTCYAN
+ 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 COLOR_WHITE
+ 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.subviews.list.edit.active = false
-
- self:initListChoices()
+ self:initListChoices(nil, args.select_token)
end
function stock_trend_color(cons)
@@ -733,6 +794,29 @@ function ConstraintList:onDeleteConstraint()
)
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 --
-------------------------------
@@ -772,14 +856,20 @@ function JobConstraints:init(args)
widgets.Label{
frame = { l = 0, b = 0 },
text = {
- { key = 'CUSTOM_N', text = ': New limit, ',
+ { key = 'CUSTOM_SHIFT_A', text = ': Add limit, ',
on_activate = self:callback('onNewConstraint') },
- { key = 'CUSTOM_X', text = ': Delete',
+ { 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') }
+ 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 }
}
},
}
@@ -873,22 +963,16 @@ function JobConstraints:onNewConstraint()
local choices = {}
for i,cons in ipairs(variants) do
local itemstr = describe_item_type(cons)
- local matstr = describe_material(cons)
- local matflags = utils.list_bitfield_flags(cons.mat_mask)
- if #matflags > 0 then
- local fstr = table.concat(matflags, '/')
- if matstr == 'any material' then
- matstr = 'any '..fstr
- else
- matstr = 'any '..fstr..' '..matstr
- end
+ 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 = 'New limit',
+ frame_title = 'Add limit',
text = 'Select one of the possible outputs:',
text_pen = COLOR_WHITE,
choices = choices,
@@ -930,7 +1014,7 @@ end
local args = {...}
-if args[1] == 'list' then
+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