From 59ec9b304e70d756cefe7f90eff986e9a1c44d06 Mon Sep 17 00:00:00 2001 From: Alexander Gavrilov Date: Wed, 24 Oct 2012 21:49:30 +0400 Subject: [PATCH] Implement adding workflow constraints from gui/workflow. --- library/lua/utils.lua | 28 +++- library/modules/Materials.cpp | 2 +- plugins/lua/workflow.lua | 261 ++++++++++++++++++++++++++++++++++ plugins/workflow.cpp | 9 +- scripts/gui/workflow.lua | 110 +++++++++++--- 5 files changed, 384 insertions(+), 26 deletions(-) diff --git a/library/lua/utils.lua b/library/lua/utils.lua index 2507c9964..8070b85de 100644 --- a/library/lua/utils.lua +++ b/library/lua/utils.lua @@ -283,6 +283,16 @@ function clone_with_default(obj,default,force) return rv end +function parse_bitfield_int(value, type_ref) + local res = {} + for i,v in ipairs(type_ref) do + if bit32.extract(value, i) ~= 0 then + res[v] = true + end + end + return res +end + -- Sort a vector or lua table function sort_vector(vector,field,cmp) local fcmp = compare_field(field,cmp) @@ -304,16 +314,26 @@ end -- Linear search -function linear_index(vector,obj) +function linear_index(vector,key,field) local min,max if df.isvalid(vector) then min,max = 0,#vector-1 else min,max = 1,#vector end - for i=min,max do - if vector[i] == obj then - return i + if field then + for i=min,max do + local obj = vector[i] + if obj[field] == key then + return i, obj + end + end + else + for i=min,max do + local obj = vector[i] + if obj == key then + return i, obj + end end end return nil diff --git a/library/modules/Materials.cpp b/library/modules/Materials.cpp index a94d49181..454fdf66d 100644 --- a/library/modules/Materials.cpp +++ b/library/modules/Materials.cpp @@ -312,7 +312,7 @@ std::string MaterialInfo::getToken() else if (index == 1) return "COAL:CHARCOAL"; } - return material->id; + return material->id + ":NONE"; case Inorganic: return "INORGANIC:" + inorganic->id; case Creature: diff --git a/plugins/lua/workflow.lua b/plugins/lua/workflow.lua index 748484052..598bf8841 100644 --- a/plugins/lua/workflow.lua +++ b/plugins/lua/workflow.lua @@ -1,5 +1,7 @@ local _ENV = mkmodule('plugins.workflow') +local utils = require 'utils' + --[[ Native functions: @@ -8,7 +10,266 @@ local _ENV = mkmodule('plugins.workflow') * setEnabled(enable) * listConstraints([job]) -> {...} * setConstraint(token, by_count, goal[, gap]) -> {...} + * deleteConstraint(token) -> true/false --]] +local reaction_id_cache = nil + +if dfhack.is_core_context then + dfhack.onStateChange[_ENV] = function(code) + if code == SC_MAP_LOADED then + reaction_id_cache = nil + end + end +end + +local function get_reaction(name) + if not reaction_id_cache then + reaction_id_cache = {} + for i,v in ipairs(df.global.world.raws.reactions) do + reaction_id_cache[v.code] = i + end + end + local id = reaction_id_cache[name] or -1 + return id, df.reaction.find(id) +end + +local job_outputs = {} + +function job_outputs.CustomReaction(callback, job) + local rid, r = get_reaction(job.reaction_name) + + for i,prod in ipairs(r.products) do + if df.reaction_product_itemst:is_instance(prod) then + local mat_type, mat_index = prod.mat_type, prod.mat_index + local mat_mask + + local get_mat_prod = prod.flags.GET_MATERIAL_PRODUCT + if get_mat_prod or prod.flags.GET_MATERIAL_SAME then + local reagent_code = prod.get_material.reagent_code + local reagent_idx, src = utils.linear_index(r.reagents, reagent_code, 'code') + if not reagent_idx then goto continue end + + local item_idx, jitem = utils.linear_index(job.job_items, reagent_idx, 'reagent_index') + if jitem then + mat_type, mat_index = jitem.mat_type, jitem.mat_index + else + if not df.reaction_reagent_itemst:is_instance(src) then goto continue end + mat_type, mat_index = src.mat_type, src.mat_index + end + + if get_mat_prod then + local mat = dfhack.matinfo.decode(mat_type, mat_index) + + mat_type, mat_index = -1, -1 + + if mat then + local p_code = prod.get_material.product_code + local rp = mat.material.reaction_product + local idx = utils.linear_index(rp.id, p_code) + if not idx then + goto continue + end + mat_type, mat_index = rp.material.mat_type[idx], rp.material.mat_index[idx] + else + if code == "SOAP_MAT" then + mat_mask = { soap = true } + end + end + end + end + + callback{ + is_craft = prod.flags.CRAFTS, + item_type = prod.item_type, item_subtype = prod.item_subtype, + mat_type = mat_type, mat_index = mat_index, mat_mask = mat_mask + } + end + ::continue:: + end +end + +local function guess_job_material(job) + if job.job_type == df.job_type.PrepareMeal then + return -1, -1, nil + end + + local mat_type, mat_index = job.mat_type, job.mat_index + local mask_whole = job.material_category.whole + local mat_mask + + local jmat = df.job_type.attrs[job.job_type].material + if jmat then + mat_type, mat_index = df.builtin_mats[jmat] or -1, -1 + if mat_type < 0 and df.job_material_category[jmat] then + mat_mask = { [jmat] = true } + end + end + + if not mat_mask and mask_whole ~= 0 then + mat_mask = utils.parse_bitfield_int(mask_whole, df.job_material_category) + if mat_mask.wood2 then + mat_mask.wood = true + mat_mask.wood2 = nil + end + end + + if mat_type < 0 and #job.job_items > 0 then + local item0 = job.job_items[0] + if #job.job_items == 1 or item0.item_type == df.item_type.PLANT then + mat_type, mat_index = item0.mat_type, item0.mat_index + + if item0.item_type == df.item_type.WOOD then + mat_mask = mat_mask or {} + mat_mask.wood = true + end + end + end + + return mat_type, mat_index, mat_mask +end + +function default_output(callback, job, mat_type, mat_index, mat_mask) + local itype = df.job_type.attrs[job.job_type].item + if itype >= 0 then + local subtype = nil + if df.item_type.attrs[itype].is_rawable then + subtype = job.item_subtype + end + callback{ + item_type = itype, item_subtype = subtype, + mat_type = mat_type, mat_index = mat_index, mat_mask = mat_mask + } + end +end + +function job_outputs.SmeltOre(callback, job) + local mat = dfhack.matinfo.decode(job.job_items[0]) + if mat and mat.inorganic then + for i,v in ipairs(mat.inorganic.metal_ore.mat_index) do + callback{ item_type = df.item_type.BAR, mat_type = 0, mat_index = v } + end + else + callback{ item_type = df.item_type.BAR, mat_type = 0 } + end +end + +function job_outputs.ExtractMetalStrands(callback, job) + local mat = dfhack.matinfo.decode(job.job_items[0]) + if mat and mat.inorganic then + for i,v in ipairs(mat.inorganic.thread_metal.mat_index) do + callback{ item_type = df.item_type.THREAD, mat_type = 0, mat_index = v } + end + else + callback{ item_type = df.item_type.THREAD, mat_type = 0 } + end +end + +function job_outputs.PrepareMeal(callback, job) + if job.mat_type ~= -1 then + for i,v in ipairs(df.global.world.raws.itemdefs.food) do + if v.level == job.mat_type then + callback{ item_type = df.item_type.FOOD, item_subtype = i } + end + end + else + callback{ item_type = df.item_type.FOOD } + end +end + +function job_outputs.MakeCrafts(callback, job) + local mat_type, mat_index, mat_mask = guess_job_material(job) + callback{ is_craft = true, mat_type = mat_type, mat_index = mat_index, mat_mask = mat_mask } +end + +local plant_products = { + BrewDrink = 'DRINK', + MillPlants = 'MILL', + ProcessPlants = 'THREAD', + ProcessPlantsBag = 'LEAVES', + ProcessPlantsBarrel = 'EXTRACT_BARREL', + ProcessPlantsVial = 'EXTRACT_VIAL', + ExtractFromPlants = 'EXTRACT_STILL_VIAL', +} + +for job,flag in pairs(plant_products) do + local ttag = 'type_'..string.lower(flag) + local itag = 'idx_'..string.lower(flag) + job_outputs[job] = function(callback, job) + local mat_type, mat_index = -1, -1 + local mat = dfhack.matinfo.decode(job.job_items[0]) + if mat and mat.plant and mat.plant.flags[flag] then + mat_type = mat.plant.material_defs[ttag] + mat_index = mat.plant.material_defs[itag] + end + default_output(callback, job, mat_type, mat_index) + end +end + +local function enum_job_outputs(callback, job) + local handler = job_outputs[df.job_type[job.job_type]] + if handler then + handler(callback, job) + else + default_output(callback, job, guess_job_material(job)) + end +end + +function listJobOutputs(job) + local res = {} + enum_job_outputs(curry(table.insert, res), job) + return res +end + +function constraintToToken(cspec) + local token + if cspec.is_craft then + token = 'CRAFTS' + else + token = df.item_type[cspec.item_type] or error('invalid item type: '..cspec.item_type) + if cspec.item_subtype and cspec.item_subtype >= 0 then + local def = dfhack.items.getSubtypeDef(cspec.item_type, cspec.item_subtype) + if def then + token = token..':'..def.id + else + error('invalid subtype '..cspec.item_subtype..' of '..token) + end + end + end + local mask_part + if cspec.mat_mask then + local lst = {} + for n,v in pairs(cspec.mat_mask) do + if v then table.insert(lst,n) end + end + mask_part = table.concat(lst, ',') + end + local mat_part + if cspec.mat_type and cspec.mat_type >= 0 then + local mat = dfhack.matinfo.decode(cspec.mat_type, cspec.mat_index or -1) + if mat then + mat_part = mat:getToken() + else + error('invalid material: '..cspec.mat_type..':'..(cspec.mat_index or -1)) + end + end + local qpart + if cspec.quality and cspec.quality > 0 then + qpart = df.item_quality[cspec.quality] or error('invalid quality: '..cspec.quality) + end + + if mask_part or mat_part or qpart then + token = token .. '/' .. (mask_part or '') + if mat_part or qpart then + token = token .. '/' .. (mat_part or '') + if qpart then + token = token .. '/' .. (qpart or '') + end + end + end + + return token +end + return _ENV diff --git a/plugins/workflow.cpp b/plugins/workflow.cpp index 29538b6c9..a6bb3a41a 100644 --- a/plugins/workflow.cpp +++ b/plugins/workflow.cpp @@ -323,6 +323,7 @@ public: void init(const std::string &str) { config.val() = str; + config.ival(0) = 10; config.ival(2) = 0; } @@ -1481,7 +1482,7 @@ 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 count = luaL_optint(L, 3, -1); int gap = luaL_optint(L, 4, -1); color_ostream &out = *Lua::GetOutput(L); @@ -1491,8 +1492,10 @@ static int setConstraint(lua_State *L) luaL_error(L, "invalid constraint: %s", token); icv->setGoalByCount(by_count); - icv->setGoalCount(count); - icv->setGoalGap(gap); + if (!lua_isnil(L, 3)) + icv->setGoalCount(count); + if (!lua_isnil(L, 4)) + icv->setGoalGap(gap); process_constraints(out); push_constraint(L, icv); diff --git a/scripts/gui/workflow.lua b/scripts/gui/workflow.lua index c5d28cb98..9dfb7dd76 100644 --- a/scripts/gui/workflow.lua +++ b/scripts/gui/workflow.lua @@ -125,10 +125,13 @@ end function describe_item_type(iobj) local itemline = 'any item' - if iobj.item_type >= 0 then + 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 def = dfhack.items.getSubtypeDef(iobj.item_type, iobj.item_subtype) - local count = dfhack.items.getSubtypeCount(iobj.item_type, iobj.item_subtype) + 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 @@ -139,14 +142,14 @@ function describe_item_type(iobj) end function is_caste_mat(iobj) - return dfhack.items.isCasteMaterial(iobj.item_type) + return dfhack.items.isCasteMaterial(iobj.item_type or -1) 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 + matline = 'no material' + elseif (iobj.mat_type or -1) >= 0 then local info = dfhack.matinfo.decode(iobj.mat_type, iobj.mat_index) if info then matline = info:toString() @@ -157,12 +160,16 @@ function describe_material(iobj) return matline end -function list_flags(list, bitfield) - for name,val in pairs(bitfield) do - if val then - table.insert(list, name) +function list_flags(bitfield) + local list = {} + if bitfield then + for name,val in pairs(bitfield) do + if val then + table.insert(list, name) + end end end + return list end function JobConstraints:initListChoices(clist) @@ -186,19 +193,13 @@ function JobConstraints:initListChoices(clist) 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 + local itemstr = describe_item_type(cons) 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) + local matflags = list_flags(cons.mat_mask) if #matflags > 0 then matflags[1] = 'any '..matflags[1] if matstr == 'any material' then @@ -284,7 +285,80 @@ function JobConstraints:onIncRange(field, delta) self:saveConstraint(cons) end +function make_constraint_variants(outputs) + local variants = {} + local known = {} + local function register(cons) + cons.token = workflow.constraintToToken(cons) + if not known[cons.token] then + known[cons.token] = true + table.insert(variants, cons) + end + end + + local generic = {} + local anymat = {} + for i,cons in ipairs(outputs) do + local mask = cons.mat_mask + if (cons.mat_type or -1) >= 0 then + cons.mat_mask = nil + end + register(cons) + if mask then + table.insert(generic, { + item_type = cons.item_type, + item_subtype = cons.item_subtype, + is_craft = cons.is_craft, + mat_mask = mask + }) + end + table.insert(anymat, { + item_type = cons.item_type, + item_subtype = cons.item_subtype, + is_craft = cons.is_craft + }) + end + for i,cons in ipairs(generic) do register(cons) end + for i,cons in ipairs(anymat) do register(cons) end + + return variants +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 = make_constraint_variants(outputs) + + local choices = {} + for i,cons in ipairs(variants) do + local itemstr = describe_item_type(cons) + local matstr = describe_material(cons) + local matflags = list_flags(cons.mat_mask or {}) + if #matflags > 0 then + local fstr = table.concat(matflags, '/') + if matstr == 'any material' then + matstr = 'any '..fstr + else + matstr = 'any '..fstr..' '..matstr + end + end + + table.insert(choices, { text = itemstr..' of '..matstr, obj = cons }) + end + + dlg.showListPrompt( + 'Job Outputs', + 'Select one of the job outputs:', + COLOR_WHITE, + choices, + function(idx,item) + self:saveConstraint(item.obj) + end + ) end function JobConstraints:onDeleteConstraint()