Files
opus-apps/milo/apis/craft2.lua
Kan18 2f67fb2ef8 Try to update Milo for 1.19
Removes Milo trying to access damage on items (`nil` because of The
Flattening). We might also need to reimplement showing durability in the
item's display name - I got a little too carried away with removing
mentions of "damage". Also renames nbtHash to nbt to be consistent with
new CC:T naming. I tried not to touch anything related to MiloRemote for
now, and there are probably still many bugs remaining that need to be
ironed out. Most of the basic functionality works now, though.
2022-12-24 15:39:14 +04:00

567 lines
14 KiB
Lua

local itemDB = require('core.itemDB')
local Tasks = require('milo.taskRunner')
local Util = require('opus.util')
local fs = _G.fs
local turtle = _G.turtle
local Craft = {
STATUS_INFO = 'info',
STATUS_WARNING = 'warning',
STATUS_ERROR = 'error',
STATUS_SUCCESS = 'success',
RECIPES_DIR = 'packages/recipeBook/etc/recipes',
USER_DIR = 'usr/etc/recipes',
USER_RECIPES = 'usr/config/recipes.db',
MACHINE_LOOKUP = 'usr/config/machine_crafting.db',
}
local function splitKey(key)
local t = Util.split(key, '(.-):')
local item = { }
if t[3] then
item.nbt = t[3]
end
item.name = table.concat(t, ':')
return item
end
local function makeRecipeKey(item)
if type(item) == 'string' then
item = splitKey(item)
end
return table.concat({ item.name, item.nbt }, ':')
end
local function convert(ingredient)
return type(ingredient) == 'table' and ingredient or {
key = ingredient,
count = 1,
}
end
local function getCraftingTool(storage, item)
local items = storage:listItems()
for _,v in pairs(items) do
if item.name == v.name and
(not item.nbt or item.nbt == v.nbt) then
return v
end
end
return item
end
function Craft.ingredients(recipe)
local i = 0
local keys = Util.keys(recipe.ingredients)
return function()
i = i + 1
local a = keys[i]
if a then
return a, convert(recipe.ingredients[a])
end
end
end
function Craft.clearGrid(storage)
local success = true
local tasks = Tasks()
for index, slot in pairs(storage.turtleInventory.adapter.list()) do
tasks:add(function()
if storage:import(storage.turtleInventory, index, slot.count, slot) ~= slot.count then
success = false
end
end)
end
tasks:run()
return success
end
function Craft.getItemCount(items, item)
if type(item) == 'string' then
item = splitKey(item)
end
local count = 0
for _,v in pairs(items) do
if v.name == item.name and
v.nbt == item.nbt then
count = count + v.count
end
end
return count
end
function Craft.sumIngredients(recipe)
-- produces { ['minecraft:planks:0'] = 8 }
local t = { }
for _,entry in pairs(recipe.ingredients) do
local item = convert(entry)
t[item.key] = (t[item.key] or 0) + item.count
end
return t
end
local function machineCraft(recipe, storage, machineName, request, count, item)
local machine = storage.nodes[machineName]
if not machine then
request.status = 'machine not found'
request.statusCode = Craft.STATUS_ERROR
return
end
if not machine.adapter or not machine.adapter.online then
request.status = 'machine offline'
request.statusCode = Craft.STATUS_ERROR
return
end
local pending = item.pending[recipe.result] or 0
local list = machine.adapter.list()
for k in pairs(recipe.ingredients) do
if list[k] then
if pending > 0 then
request.status = 'processing'
request.statusCode = Craft.STATUS_INFO
else
request.status = 'machine in use'
request.statusCode = Craft.STATUS_WARNING
end
return
end
end
if count > 0 then
local xferred = { }
for k,v in pairs(recipe.ingredients) do
local entry = convert(v)
local provided = storage:export(machine, k, count * entry.count, splitKey(entry.key))
xferred[k] = {
key = entry.key,
count = provided,
}
if provided ~= count * entry.count then
-- take back out whatever we put in
for k2,v2 in pairs(xferred) do
if v2.count > 0 then
storage:import(machine, k2, v2.count, splitKey(v2.key))
end
end
request.status = 'Invalid recipe'
request.statusCode = Craft.STATUS_ERROR
return
end
end
end
request.status = 'processing'
request.statusCode = Craft.STATUS_INFO
item.pending[recipe.result] = pending + (count * recipe.count)
end
local function turtleCraft(recipe, storage, request, count)
if not Craft.clearGrid(storage) then
request.status = 'grid in use'
request.statusCode = Craft.STATUS_ERROR
return
end
local failed
local tasks = Tasks()
for k,v in pairs(recipe.ingredients) do
local item = splitKey(v)
if recipe.craftingTools and recipe.craftingTools[v] then
item = getCraftingTool(storage, item)
end
tasks:add(function()
if storage:export(storage.turtleInventory, k, count, item) ~= count then
request.status = 'rescan needed ?'
request.statusCode = Craft.STATUS_ERROR
failed = true
_G._syslog('failed to export: ' .. item.name)
-- _G._p4 = recipe
-- _G._p = storage
-- _G._p2 = request
--read()
end
end)
end
tasks:run()
if failed then
Craft.clearGrid(storage)
return
end
turtle.select(1)
if turtle.craft() then
local l = storage.turtleInventory.adapter.list()
local crafted = l[1]
if recipe.result ~= itemDB:makeKey(crafted) then
_G._syslog('expected: ' .. recipe.result)
_G._syslog('got: ' .. itemDB:makeKey(crafted))
request.aborted = true
request.status = 'Failed to craft: ' .. recipe.result
request.statusCode = Craft.STATUS_ERROR
else
request.crafted = request.crafted + count * recipe.count
--request.status = 'crafted'
--request.statusCode = Craft.STATUS_SUCCESS
end
else
_G._syslog('just failed')
request.status = 'Failed to craft'
request.statusCode = Craft.STATUS_ERROR
end
Craft.clearGrid(storage)
return request.statusCode == Craft.STATUS_SUCCESS
end
function Craft.processPending(item, storage)
for _, key in pairs(Util.keys(item.pending)) do
local count = item.pending[key]
local imported = storage.activity[key]
if imported then
local amount = math.min(imported, count)
storage.activity[key] = imported - amount
item.pending[key] = count - amount
item.ingredients[key].crafted = item.ingredients[key].crafted + amount
if item.pending[key] <= 0 then
item.pending[key] = nil
end
end
end
end
-- return a recipe if the ingredients will not produce recursion
local function findValidRecipe(key, path)
local recipe = Craft.findRecipe(key)
if recipe then
for k in pairs(Craft.sumIngredients(recipe)) do
if path[k] then
return
end
end
return recipe
end
end
function Craft.craftRecipe(recipe, count, storage, origItem)
if type(recipe) == 'string' then
recipe = Craft.recipes[recipe]
if not recipe then
return 0, 'No recipe'
end
end
local path = { [ recipe.result ] = true }
return Craft.craftRecipeInternal(recipe, count, storage, origItem, path)
end
local function adjustCounts(recipe, count, ingredients, storage)
-- decrement ingredients used
for key,icount in pairs(Craft.sumIngredients(recipe)) do
ingredients[key].count = ingredients[key].count - (icount * count)
end
-- increment crafted
local result = ingredients[recipe.result]
result.count = result.count + (count * recipe.count)
end
function Craft.craftRecipeInternal(recipe, count, storage, origItem, path)
local request = origItem.ingredients[recipe.result]
--[[
if origItem.pending[recipe.result] then
request.status = 'processing'
request.statusCode = Craft.STATUS_INFO
return 0
end
--]]
count = count - (origItem.pending[recipe.result] or 0)
local canCraft = Craft.getCraftableAmount(recipe, count, origItem.ingredients)
if not origItem.forceCrafting and canCraft == 0 then
return 0
end
canCraft = math.ceil(canCraft / recipe.count)
if origItem.forceCrafting then
count = math.ceil(count / recipe.count)
else
count = canCraft
end
local function maxBatch()
local max = 64
for _, i in Craft.ingredients(recipe) do
max = math.min(max, math.floor(itemDB:getMaxCount(i.key) / i.count))
end
return max
end
local maxCount = recipe.maxCount or math.floor(64 / recipe.count)
maxCount = math.min(maxCount, maxBatch())
repeat
local craftedIngredient
local abort
for key,icount in pairs(Craft.sumIngredients(recipe)) do
local itemCount = Craft.getItemCount(origItem.ingredients, key)
local need = icount * count
if recipe.craftingTools and recipe.craftingTools[key] then
need = 1
end
maxCount = math.min(maxCount, itemDB:getMaxCount(key))
if itemCount < need then
local irecipe = findValidRecipe(key, path)
if not irecipe then
abort = true
else
local iqty = need - itemCount
local p = Util.shallowCopy(path)
p[irecipe.result] = true
local crafted = Craft.craftRecipeInternal(irecipe, iqty, storage, origItem, p)
if not origItem.forceCrafting and crafted < iqty then
abort = true
end
if origItem.forceCrafting and crafted < iqty then
canCraft = math.min(canCraft, math.floor((itemCount + crafted) / icount))
end
if crafted > 0 then
craftedIngredient = true
end
end
end
end
if abort then
return 0
end
until not craftedIngredient
local crafted = 0
while canCraft > 0 do
local batch = math.min(canCraft, maxCount)
local machine = Craft.machineLookup[recipe.result]
if machine then
if not machineCraft(recipe, storage, machine, request, batch, origItem) then
break
end
elseif not turtleCraft(recipe, storage, request, batch) then
break
end
adjustCounts(recipe, batch, origItem.ingredients, storage)
crafted = crafted + batch
canCraft = canCraft - maxCount
end
if request.aborted then
origItem.aborted = true
return 0
end
return crafted * recipe.count
end
function Craft.findRecipe(key)
if type(key) ~= 'string' then
key = itemDB:makeKey(key)
end
local item = itemDB:splitKey(key)
return Craft.recipes[makeRecipeKey(item)]
end
-- determine the full list of ingredients needed to craft
-- a quantity of a recipe.
function Craft.getResourceList(inRecipe, items, inCount, pending)
local summed = { }
local function sumItems(recipe, key, count, path)
local item = itemDB:splitKey(key)
local summedItem = summed[key]
if not summedItem then
summedItem = Util.shallowCopy(item)
summedItem.count = Craft.getItemCount(items, item)
summedItem.displayName = itemDB:getName(item)
summedItem.total = 0
summedItem.need = 0
summedItem.used = 0
summed[key] = summedItem
summedItem.recipe = findValidRecipe(key, path)
end
local total = count
local used = math.min(summedItem.count, total)
local need = total - used
if pending and pending[key] then
need = need - pending[key]
end
if recipe.craftingTools and recipe.craftingTools[key] then
summedItem.total = 1
if summedItem.count > 0 then
summedItem.used = 1
summedItem.need = 0
need = 0
elseif not summedItem.recipe then
summedItem.need = 1
need = 1
else
need = 1
end
else
summedItem.total = summedItem.total + total
summedItem.count = summedItem.count - used
summedItem.used = summedItem.used + used
if not summedItem.recipe then
summedItem.need = summedItem.need + need
end
end
if need > 0 and summedItem.recipe then
need = math.ceil(need / summedItem.recipe.count)
local p = Util.shallowCopy(path)
p[summedItem.recipe.result] = true
for ikey,iqty in pairs(Craft.sumIngredients(summedItem.recipe)) do
sumItems(summedItem.recipe, ikey, math.ceil(need * iqty), p)
end
end
end
inCount = math.ceil(inCount / inRecipe.count)
if pending and pending[inRecipe.result] then
inCount = inCount - pending[inRecipe.result]
end
if inCount > 0 then
local path = { [ inRecipe.result ] = true }
for ikey,iqty in pairs(Craft.sumIngredients(inRecipe)) do
sumItems(inRecipe, ikey, math.ceil(inCount * iqty), path)
end
end
return summed
end
function Craft.getResourceList4(inRecipe, items, count)
local summed = Craft.getResourceList(inRecipe, items, count)
-- filter down to just raw materials
return Util.filter(summed, function(a) return a.used > 0 or a.need > 0 end)
end
-- given a certain quantity, return how many of those can be crafted
function Craft.getCraftableAmount(inRecipe, inCount, items, missing)
local function sumItems(recipe, summedItems, count, path)
local canCraft = 0
for _ = 1, count do
for _,entry in pairs(recipe.ingredients) do
local item = convert(entry)
local summedItem = summedItems[item.key] or Craft.getItemCount(items, item.key)
local irecipe = findValidRecipe(item.key, path)
if irecipe and summedItem <= 0 then
local p = Util.shallowCopy(path)
p[irecipe.result] = true
summedItem = summedItem + sumItems(irecipe, summedItems, item.count, p)
end
if summedItem <= 0 then
if missing and not irecipe then
missing.name = item.key
end
return canCraft
end
if not recipe.craftingTools or not recipe.craftingTools[item.key] then
summedItems[item.key] = summedItem - item.count
end
end
canCraft = canCraft + recipe.count
end
return canCraft
end
local path = { [ inRecipe.result ] = true }
return sumItems(inRecipe, { }, math.ceil(inCount / inRecipe.count), path)
end
-- doesnt work :( solar panel 3 fails (qty 2)
function Craft.getCraftableAmountXXX(inRecipe, limit, items)
local allIngredients = { }
local function sumItems(recipe, count, path)
local canCraft = 0
for _ = 1, count do
for iKey, iCount in pairs(Craft.sumIngredients(recipe)) do
local quantity = allIngredients[iKey] or Craft.getItemCount(items, iKey)
if quantity < iCount then
local irecipe = findValidRecipe(iKey, path)
if irecipe then
local p = Util.shallowCopy(path)
p[irecipe.result] = true
quantity = quantity + sumItems(irecipe, iCount - quantity, p)
end
end
if quantity < iCount then
return canCraft
end
if not recipe.craftingTools or not recipe.craftingTools[iKey] then
allIngredients[iKey] = quantity - iCount
end
end
canCraft = canCraft + recipe.count
end
return canCraft
end
local path = { [ inRecipe.result ] = true }
return sumItems(inRecipe, math.ceil(limit / inRecipe.count), path)
end
function Craft.loadRecipes()
Craft.recipes = { }
Util.merge(Craft.recipes, (Util.readTable(fs.combine(Craft.RECIPES_DIR, 'minecraft.db')) or { }).recipes)
if fs.exists(Craft.USER_DIR) then
for _, file in pairs(fs.list(Craft.USER_DIR)) do
local recipeFile = Util.readTable(fs.combine(Craft.USER_DIR, file))
Util.merge(Craft.recipes, recipeFile.recipes)
end
end
local recipes = Util.readTable(Craft.USER_RECIPES) or { }
Util.merge(Craft.recipes, recipes)
for k,v in pairs(Craft.recipes) do
v.result = k
end
Craft.machineLookup = Util.readTable(Craft.MACHINE_LOOKUP) or { }
end
function Craft.canCraft(item, count, items)
return Craft.getCraftableAmount(Craft.recipes[item], count, items) == count
end
Craft.loadRecipes()
return Craft