local _ENV = mkmodule('plugins.workflow') local utils = require 'utils' --[[ Native functions: * isEnabled() * setEnabled(enable) * listConstraints([job[,with_history] ]) -> {{...},...} * findConstraint(token) -> {...} or nil * setConstraint(token[, by_count, goal, gap]) -> {...} * deleteConstraint(token) -> true/false * getCountHistory(token) -> {{...},...} or nil --]] 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.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) if not r then return end 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 p_code = prod.get_material.product_code local mat = dfhack.matinfo.decode(mat_type, mat_index) mat_type, mat_index = -1, -1 if mat then local rp = mat.material.reaction_product local idx = utils.linear_index(rp.id, p_code, 'value') if idx then mat_type, mat_index = rp.material.mat_type[idx], rp.material.mat_index[idx] end else if p_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.dfhack_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.dfhack_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, item_type = -1, item_subtype = -1, 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 tag = string.lower(flag) job_outputs[job] = function(callback, job) local mat_type, mat_index = -1, -1 local seed_type, seed_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.type[tag] mat_index = mat.plant.material_defs.idx[tag] seed_type = mat.plant.material_defs.type.seed seed_index = mat.plant.material_defs.idx.seed end local mat_mask = { } if flag ~= 'LEAVES' then mat_mask.plant = true end default_output(callback, job, mat_type, mat_index, mat_mask) callback{ item_type = df.item_type.SEEDS, mat_type = seed_type, mat_index = seed_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 doEnumJobOutputs(native_cb, job) local function cb(info) native_cb( info.item_type, info.item_subtype, info.mat_mask, info.mat_type, info.mat_index, info.is_craft ) end enum_job_outputs(cb, job) 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 mask_part = string.upper(table.concat(utils.list_bitfield_flags(cspec.mat_mask), ',')) 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 qlist = {} if cspec.is_local then table.insert(qlist, "LOCAL") end if cspec.min_quality and cspec.min_quality > 0 then local qn = df.item_quality[cspec.min_quality] or error('invalid quality: '..cspec.min_quality) table.insert(qlist, qn) end local qpart if #qlist > 0 then qpart = table.concat(qlist, ',') 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 function listWeakenedConstraints(outputs) local variants = {} local known = {} local function register(cons) cons.token = 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 local info = dfhack.matinfo.decode(cons) if info then for i,flag in ipairs(df.dfhack_material_category) do if flag and flag ~= 'wood2' and info:matches{[flag]=true} then mask = mask or {} mask[flag] = true end end end end register(cons) if mask then for k,v in pairs(mask) do table.insert(generic, { item_type = cons.item_type, item_subtype = cons.item_subtype, is_craft = cons.is_craft, mat_mask = { [k] = v } }) end 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 return _ENV