diff --git a/NEWS b/NEWS index 5567258bc..b4712c22d 100644 --- a/NEWS +++ b/NEWS @@ -13,6 +13,7 @@ DFHack future New GUI scripts: - gui/guide-path: displays the cached path for minecart Guide orders. - gui/workshop-job: displays inputs of a workshop job and allows tweaking them. + - gui/workflow: a front-end for the workflow plugin. DFHack v0.34.11-r2 diff --git a/dfhack.init-example b/dfhack.init-example index 27801b73d..d4c7dc233 100644 --- a/dfhack.init-example +++ b/dfhack.init-example @@ -78,6 +78,9 @@ keybinding add Alt-P@dwarfmode/Hauling/DefineStop/Cond/Guide gui/guide-path # workshop job details keybinding add Alt-A@dwarfmode/QueryBuilding/Some/Workshop/Job gui/workshop-job +# workflow front-end +keybinding add Ctrl-W@dwarfmode/QueryBuilding/Some/Workshop/Job gui/workflow + ############################ # UI and game logic tweaks # ############################ diff --git a/library/LuaApi.cpp b/library/LuaApi.cpp index aa6c93c32..593ac3d8d 100644 --- a/library/LuaApi.cpp +++ b/library/LuaApi.cpp @@ -500,7 +500,9 @@ static void OpenPersistent(lua_State *state) * Material info lookup * ************************/ -static void push_matinfo(lua_State *state, MaterialInfo &info) +static int DFHACK_MATINFO_TOKEN = 0; + +void Lua::Push(lua_State *state, MaterialInfo &info) { if (!info.isValid()) { @@ -509,7 +511,7 @@ static void push_matinfo(lua_State *state, MaterialInfo &info) } lua_newtable(state); - lua_pushvalue(state, lua_upvalueindex(1)); + lua_rawgetp(state, LUA_REGISTRYINDEX, &DFHACK_MATINFO_TOKEN); lua_setmetatable(state, -2); lua_pushinteger(state, info.type); @@ -564,7 +566,7 @@ static int dfhack_matinfo_find(lua_State *state) info.find(tokens); } - push_matinfo(state, info); + Lua::Push(state, info); return 1; } @@ -632,7 +634,7 @@ static int dfhack_matinfo_decode(lua_State *state) { MaterialInfo info; decode_matinfo(state, &info, true); - push_matinfo(state, info); + Lua::Push(state, info); return 1; } @@ -711,6 +713,9 @@ static void OpenMatinfo(lua_State *state) { luaL_getsubtable(state, lua_gettop(state), "matinfo"); + lua_dup(state); + lua_rawsetp(state, LUA_REGISTRYINDEX, &DFHACK_MATINFO_TOKEN); + lua_dup(state); luaL_setfuncs(state, dfhack_matinfo_funcs, 1); diff --git a/library/include/LuaTools.h b/library/include/LuaTools.h index fddbdbc26..ec4917972 100644 --- a/library/include/LuaTools.h +++ b/library/include/LuaTools.h @@ -36,6 +36,7 @@ distribution. namespace DFHack { class function_identity_base; + struct MaterialInfo; namespace Units { struct NoblePosition; @@ -283,6 +284,7 @@ namespace DFHack {namespace Lua { DFHACK_EXPORT void Push(lua_State *state, df::coord obj); DFHACK_EXPORT void Push(lua_State *state, df::coord2d obj); void Push(lua_State *state, const Units::NoblePosition &pos); + DFHACK_EXPORT void Push(lua_State *state, MaterialInfo &info); template inline void Push(lua_State *state, T *ptr) { PushDFObject(state, ptr); } diff --git a/plugins/lua/workflow.lua b/plugins/lua/workflow.lua new file mode 100644 index 000000000..748484052 --- /dev/null +++ b/plugins/lua/workflow.lua @@ -0,0 +1,14 @@ +local _ENV = mkmodule('plugins.workflow') + +--[[ + + Native functions: + + * isEnabled() + * setEnabled(enable) + * listConstraints([job]) -> {...} + * setConstraint(token, by_count, goal[, gap]) -> {...} + +--]] + +return _ENV diff --git a/plugins/workflow.cpp b/plugins/workflow.cpp index 2720baa83..af35c6533 100644 --- a/plugins/workflow.cpp +++ b/plugins/workflow.cpp @@ -4,6 +4,9 @@ #include "PluginManager.h" #include "MiscUtils.h" +#include "LuaTools.h" +#include "DataFuncs.h" + #include "modules/Materials.h" #include "modules/Items.h" #include "modules/Gui.h" @@ -743,6 +746,20 @@ static void delete_constraint(ItemConstraint *cv) delete cv; } +static bool deleteConstraint(std::string name) +{ + for (size_t i = 0; i < constraints.size(); i++) + { + if (constraints[i]->config.val() != name) + continue; + + delete_constraint(constraints[i]); + return true; + } + + return false; +} + /****************************** * JOB-CONSTRAINT MAPPING * ******************************/ @@ -1347,6 +1364,153 @@ static void process_constraints(color_ostream &out) update_jobs_by_constraints(out); } +static void update_data_structures(color_ostream &out) +{ + if (enabled) { + check_lost_jobs(out, 0); + recover_jobs(out); + update_job_data(out); + map_job_constraints(out); + map_job_items(out); + } +} + +/************* + * LUA API * + *************/ + +static bool isEnabled() { return enabled; } + +static void setEnabled(color_ostream &out, bool enable) +{ + if (enable && !enabled) + { + enable_plugin(out); + } + else if (!enable && enabled) + { + enabled = false; + setOptionEnabled(CF_ENABLED, false); + stop_protect(out); + } +} + +static void push_constraint(lua_State *L, ItemConstraint *cv) +{ + lua_newtable(L); + int ctable = lua_gettop(L); + + Lua::SetField(L, cv->config.entry_id(), ctable, "id"); + Lua::SetField(L, cv->config.val(), ctable, "token"); + + // Constraint key + + Lua::SetField(L, cv->item.type, ctable, "item_type"); + Lua::SetField(L, cv->item.subtype, ctable, "item_subtype"); + + Lua::SetField(L, cv->is_craft, ctable, "is_craft"); + + Lua::PushDFObject(L, &cv->mat_mask); + lua_setfield(L, -2, "mat_mask"); + + Lua::SetField(L, cv->material.type, ctable, "mat_type"); + Lua::SetField(L, cv->material.index, ctable, "mat_index"); + + Lua::SetField(L, (int)cv->min_quality, ctable, "min_quality"); + + // Constraint value + + Lua::SetField(L, cv->goalByCount(), ctable, "goal_by_count"); + Lua::SetField(L, cv->goalCount(), ctable, "goal_value"); + Lua::SetField(L, cv->goalGap(), ctable, "goal_gap"); + + Lua::SetField(L, cv->item_amount, ctable, "cur_amount"); + Lua::SetField(L, cv->item_count, ctable, "cur_count"); + Lua::SetField(L, cv->item_inuse, ctable, "cur_in_use"); + + // Current state value + + if (cv->request_resume) + Lua::SetField(L, "resume", ctable, "request"); + else if (cv->request_suspend) + Lua::SetField(L, "suspend", ctable, "request"); + + lua_newtable(L); + + for (size_t i = 0, j = 0; i < cv->jobs.size(); i++) + { + if (!cv->jobs[i]->isLive()) continue; + Lua::PushDFObject(L, cv->jobs[i]->actual_job); + lua_rawseti(L, -2, ++j); + } + + lua_setfield(L, ctable, "jobs"); +} + +static int listConstraints(lua_State *L) +{ + auto job = Lua::CheckDFObject(L, 1); + ProtectedJob *pj = NULL; + if (job) + pj = get_known(job->id); + + if (!enabled || (job && !pj)) + { + lua_pushnil(L); + return 1; + } + + color_ostream &out = *Lua::GetOutput(L); + update_data_structures(out); + + lua_newtable(L); + + auto &vec = (pj ? pj->constraints : constraints); + + for (size_t i = 0; i < vec.size(); i++) + { + push_constraint(L, vec[i]); + lua_rawseti(L, -2, i+1); + } + + return 1; +} + +static int setConstraint(lua_State *L) +{ + auto token = luaL_checkstring(L, 1); + bool by_count = lua_toboolean(L, 2); + int count = luaL_checkint(L, 3); + int gap = luaL_optint(L, 4, -1); + + color_ostream &out = *Lua::GetOutput(L); + + ItemConstraint *icv = get_constraint(out, token); + if (!icv) + luaL_error(L, "invalid constraint: %s", token); + + icv->setGoalByCount(by_count); + icv->setGoalCount(count); + icv->setGoalGap(gap); + + process_constraints(out); + push_constraint(L, icv); + return 1; +} + +DFHACK_PLUGIN_LUA_FUNCTIONS { + DFHACK_LUA_FUNCTION(isEnabled), + DFHACK_LUA_FUNCTION(setEnabled), + DFHACK_LUA_FUNCTION(deleteConstraint), + DFHACK_LUA_END +}; + +DFHACK_PLUGIN_LUA_COMMANDS { + DFHACK_LUA_COMMAND(listConstraints), + DFHACK_LUA_COMMAND(setConstraint), + DFHACK_LUA_END +}; + /****************************** * PRINTING AND THE COMMAND * ******************************/ @@ -1490,13 +1654,7 @@ static command_result workflow_cmd(color_ostream &out, vector & paramet return CR_FAILURE; } - if (enabled) { - check_lost_jobs(out, 0); - recover_jobs(out); - update_job_data(out); - map_job_constraints(out); - map_job_items(out); - } + update_data_structures(out); df::building *workshop = NULL; //FIXME: unused variable! @@ -1514,18 +1672,11 @@ static command_result workflow_cmd(color_ostream &out, vector & paramet if (cmd == "enable" || cmd == "disable") { bool enable = (cmd == "enable"); - if (enable && !enabled) - { - enable_plugin(out); - } - else if (!enable && parameters.size() == 1) + if (enable) + setEnabled(out, true); + else if (parameters.size() == 1) { - if (enabled) - { - enabled = false; - setOptionEnabled(CF_ENABLED, false); - stop_protect(out); - } + setEnabled(out, false); out << "The plugin is disabled." << endl; return CR_OK; @@ -1643,14 +1794,8 @@ static command_result workflow_cmd(color_ostream &out, vector & paramet if (parameters.size() != 2) return CR_WRONG_USAGE; - for (size_t i = 0; i < constraints.size(); i++) - { - if (constraints[i]->config.val() != parameters[1]) - continue; - - delete_constraint(constraints[i]); + if (deleteConstraint(parameters[1])) return CR_OK; - } out.printerr("Constraint not found: %s\n", parameters[1].c_str()); return CR_FAILURE; diff --git a/scripts/gui/workflow.lua b/scripts/gui/workflow.lua new file mode 100644 index 000000000..17413d46e --- /dev/null +++ b/scripts/gui/workflow.lua @@ -0,0 +1,202 @@ +-- A GUI front-end for the workflow plugin. + +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, + curry(function(...) + workflow.setEnabled(true) + return cb(...) + end,...) + ) + end +end + +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) + + local status = { text = 'No worker', pen = COLOR_DARKGREY } + local worker = dfhack.job.getWorker(self.job) + if self.job.flags.suspend then + status = { text = 'Suspended', pen = COLOR_RED } + elseif worker then + status = { text = dfhack.TranslateName(dfhack.units.getVisibleName(worker)), pen = COLOR_GREEN } + end + + self:addviews{ + widgets.Label{ + frame = { l = 0, t = 0 }, + text = { + 'Workflow Constraints' + } + }, + widgets.List{ + view_id = 'list', + frame = { t = 2, b = 2 }, + row_height = 4, + scroll_keys = widgets.SECONDSCROLL, + }, + widgets.Label{ + frame = { l = 0, b = 0 }, + text = { + { key = 'LEAVESCREEN', text = ': Back', + on_activate = self:callback('dismiss') } + } + }, + } + + self:initListChoices(args.clist) +end + +function JobConstraints:onGetSelectedBuilding() + return self.building +end + +function JobConstraints:onGetSelectedJob() + return self.job +end + +function describe_item_type(iobj) + local itemline = 'any item' + if iobj.item_type >= 0 then + itemline = df.item_type.attrs[iobj.item_type].caption or iobj.item_type + local def = dfhack.items.getSubtypeDef(iobj.item_type, iobj.item_subtype) + local count = dfhack.items.getSubtypeCount(iobj.item_type, iobj.item_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) +end + +function describe_material(iobj) + local matline = 'any material' + if is_caste_mat(iobj) then + matline = 'material not applicable' + elseif iobj.mat_type >= 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 list_flags(list, bitfield) + for name,val in pairs(bitfield) do + if val then + table.insert(list, name) + end + end +end + +function JobConstraints:initListChoices(clist) + 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_RED + end + local itemstr + if cons.is_craft then + itemstr = 'any craft' + else + itemstr = describe_item_type(cons) + end + if cons.min_quality > 0 then + itemstr = itemstr .. ' ('..df.item_quality[cons.min_quality]..')' + end + local matstr = describe_material(cons) + local matflagstr = '' + local matflags = {} + list_flags(matflags, cons.mat_mask) + if #matflags > 0 then + matflagstr = 'class: '..table.concat(matflags, ', ') + end + + table.insert(choices, { + text = { + goal, ' ', { text = '(now '..curval..')', pen = order_pen }, NEWLINE, + ' ', itemstr, NEWLINE, ' ', matstr, NEWLINE, ' ', matflagstr + }, + obj = cons + }) + end + + self.subviews.list:setChoices(choices) +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 + +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 + +check_enabled(function() + local job = dfhack.gui.getSelectedJob() + if not job.flags['repeat'] then + dlg.showMessage('Not Supported', 'Workflow only tracks repeat jobs.', COLOR_LIGHTRED) + return + end + 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) +