UNPKG

mineflayer-crafting-util

Version:
602 lines (601 loc) 29.8 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports._build = _build; exports.buildStatic = buildStatic; function recipeInputs(recipe) { if (recipe.ingredients != null && recipe.ingredients.length > 0) return recipe.ingredients; return recipe.delta.filter((item) => item.count < 0); } const strictItemMatcher = (wantedId, availableId) => wantedId === availableId; function availableItemCount(availableItems, itemId, itemMatches = strictItemMatcher) { return availableItems .filter((item) => itemMatches(itemId, item.id)) .reduce((total, item) => total + item.count, 0); } function canCraftRecipe(availableItems, recipe, recipeApplications, itemMatches = strictItemMatcher) { return recipeInputs(recipe).every((input) => availableItemCount(availableItems, input.id, itemMatches) >= -input.count * recipeApplications); } function recipeAvailabilityScore(availableItems, recipe, recipeApplications, itemMatches = strictItemMatcher) { return recipeInputs(recipe).reduce((score, input) => { const required = -input.count * recipeApplications; const strictAvailable = Math.min(required, availableItemCount(availableItems, input.id)); const matchedAvailable = Math.min(required, availableItemCount(availableItems, input.id, itemMatches)); return score + strictAvailable * 2 + matchedAvailable; }, 0); } function findBestCraftableRecipe(availableItems, recipes, count, itemMatches = strictItemMatcher) { var _a; return (_a = recipes .map((recipe, index) => { const recipeApplications = Math.ceil(count / recipe.result.count); const craftable = canCraftRecipe(availableItems, recipe, recipeApplications, itemMatches); const score = craftable ? recipeAvailabilityScore(availableItems, recipe, recipeApplications, itemMatches) : -1; return { recipe, index, score }; }) .filter(({ score }) => score >= 0) .sort((a, b) => b.score - a.score || a.index - b.index)[0]) === null || _a === void 0 ? void 0 : _a.recipe; } function applyRecipeResults(items, recipesToDo, itemMatches = strictItemMatcher) { for (const toDo of recipesToDo) { for (const item of toDo.recipe.delta) { if (item.count < 0) { let count = -item.count * toDo.recipeApplications; for (const existingItem of items) { if (!itemMatches(item.id, existingItem.id)) continue; const consumed = Math.min(existingItem.count, count); existingItem.count -= consumed; count -= consumed; if (count <= 0) break; } continue; } const index = items.findIndex((e) => e.id === item.id); if (index !== -1) { items[index].count += item.count * toDo.recipeApplications; } else { items.push({ id: item.id, count: item.count * toDo.recipeApplications }); } } } } function replaceItems(targetItems, sourceItems) { targetItems.splice(0, targetItems.length, ...sourceItems.map((item) => (Object.assign({}, item)))); } function addItem(items, item) { const existing = items.find((existingItem) => existingItem.id === item.id); if (existing != null) { existing.count += item.count; } else { items.push(Object.assign({}, item)); } } function reserveItem(items, itemId, count, itemMatches = strictItemMatcher) { const reservedItems = items.map((item) => (Object.assign({}, item))); for (const item of reservedItems) { if (!itemMatches(itemId, item.id)) continue; const reserved = Math.min(item.count, count); item.count -= reserved; count -= reserved; if (count <= 0) break; } return reservedItems.filter((item) => item.count > 0); } function cloneAvailableItems(items) { return items .filter((item) => item.count > 0) .map((item) => (Object.assign({}, item))); } function addItems(items, itemToAdd) { const item = items.find((item) => item.id === itemToAdd.id); if (item != null) { item.count += itemToAdd.count; } else { items.push(Object.assign({}, itemToAdd)); } } function mergeItems(items) { const merged = []; for (const item of items) { if (item.count > 0) addItems(merged, item); } return merged; } function summarizeItemsCreated(recipesToDo) { var _a; const map = new Map(); for (const toDo of recipesToDo) { for (const item of toDo.recipe.delta) { map.set(item.id, ((_a = map.get(item.id)) !== null && _a !== void 0 ? _a : 0) + item.count * toDo.recipeApplications); } } return Array.from(map.entries()) .filter(([, count]) => count > 0) .map(([id, count]) => ({ id, count })); } function isCompleteStatus(status) { return status === 'complete'; } function deriveStatus(complete, recipesToDo) { if (complete) return 'complete'; return recipesToDo.length > 0 ? 'partial_complete' : 'failure'; } function createPlanResult(complete, itemsRequiredBase, recipesToDo, itemsCreated = summarizeItemsCreated(recipesToDo), itemsRequiredImmediate = itemsRequiredBase, itemsRemaining = []) { return { status: deriveStatus(complete, recipesToDo), itemsRequiredBase, itemsRequiredImmediate, itemsRemaining, itemsCreated, recipesToDo }; } function isConcretePlanForAvailableItems(plan, availableItems, itemMatches = strictItemMatcher) { if (!isCompleteStatus(plan.status)) return true; if (!Array.isArray(plan.recipesToDo)) return false; const currentItems = cloneAvailableItems(availableItems); for (const toDo of plan.recipesToDo) { if (toDo == null || toDo.recipe == null || toDo.recipe.result == null || !Array.isArray(toDo.recipe.delta) || !Number.isFinite(toDo.recipeApplications) || toDo.recipeApplications <= 0) { return false; } if (!canCraftRecipe(currentItems, toDo.recipe, toDo.recipeApplications, itemMatches)) return false; applyRecipeResults(currentItems, [toDo], itemMatches); } return true; } function _build(Recipe) { const acceptedItemIds = new Map(); function getAcceptedItemIds(itemId) { var _a; const cached = acceptedItemIds.get(itemId); if (cached != null) return cached; const ids = new Set([itemId]); for (const recipe of Recipe.find(itemId, null)) { if (((_a = recipe.result) === null || _a === void 0 ? void 0 : _a.id) != null) ids.add(recipe.result.id); } acceptedItemIds.set(itemId, ids); return ids; } function itemMatchesIngredient(ingredientId, availableId) { return getAcceptedItemIds(ingredientId).has(availableId); } function canProvideIngredientDirectly(item, availableItems) { if (availableItemCount(availableItems, item.id, itemMatchesIngredient) >= item.count) return true; return Recipe.find(item.id, null).some((recipe) => { const recipeApplications = Math.ceil(item.count / recipe.result.count); return canCraftRecipe(availableItems, recipe, recipeApplications, itemMatchesIngredient); }); } function getRecipeDeficits(availableItems, recipe, recipeApplications) { return recipeInputs(recipe) .map((input) => { const required = -input.count * recipeApplications; const available = availableItemCount(availableItems, input.id, itemMatchesIngredient); return { id: input.id, count: required - available }; }) .filter((item) => item.count > 0); } function findPlannedRecipe(itemId, recipesToDo) { for (let i = recipesToDo.length - 1; i >= 0; i--) { if (recipesToDo[i].recipe.result.id === itemId) return recipesToDo[i].recipe; } return undefined; } function getBaseRequirements(items, opts, recipesToDo = [], seen = new Set()) { const requirements = []; for (const item of items) { const recipe = findPlannedRecipe(item.id, recipesToDo); if (recipe != null && !seen.has(item.id)) { seen.add(item.id); const recipeApplications = Math.ceil(item.count / recipe.result.count); const inputs = recipeInputs(recipe).map((input) => ({ id: input.id, count: -input.count * recipeApplications })); for (const required of getBaseRequirements(inputs, opts, recipesToDo, seen)) { addItems(requirements, required); } seen.delete(item.id); continue; } const data = _newCraft(item, Object.assign(Object.assign({}, opts), { availableItems: undefined }), new Map()); for (const required of data.itemsRequiredBase) addItems(requirements, required); } return mergeItems(requirements); } function getPartialRequirements(itemId, remainingCount, availableItems, recipesToDo, opts) { const remaining = [{ id: itemId, count: remainingCount }]; const currentItems = cloneAvailableItems(availableItems); applyRecipeResults(currentItems, recipesToDo, itemMatchesIngredient); let completionRecipe; for (let i = recipesToDo.length - 1; i >= 0; i--) { if (recipesToDo[i].recipe.result.id === itemId) { completionRecipe = recipesToDo[i].recipe; break; } } if (completionRecipe == null) { return { itemsRequiredBase: remaining, itemsRequiredImmediate: remaining, itemsRemaining: remaining }; } const recipeApplications = Math.ceil(remainingCount / completionRecipe.result.count); const immediate = mergeItems(getRecipeDeficits(currentItems, completionRecipe, recipeApplications)); const base = getBaseRequirements(immediate, opts, recipesToDo); return { itemsRequiredBase: base, itemsRequiredImmediate: immediate, itemsRemaining: remaining }; } function _newCraft(item, opts = {}, seen = new Map(), target = item.count) { var _a, _b, _c, _d; const id = item.id; let recipes = Recipe.find(id, null); const availableItems = opts.availableItems; const includeRecursion = (_a = opts.includeRecursion) !== null && _a !== void 0 ? _a : false; const multipleRecipes = (_b = opts.multipleRecipes) !== null && _b !== void 0 ? _b : false; let matchingItem; let recipeWanted; let count = item.count; const ret0 = []; const ret1 = []; // disregard recipes that combine the item itself back together, as that is pointless for our usecase. recipes = recipes.filter((r) => r.delta.slice(0, -1).some((e) => e.id !== id)); if (availableItems !== undefined) { matchingItem = availableItems.find((e) => itemMatchesIngredient(id, e.id) && e.count >= target); if (matchingItem != null) { if (matchingItem.count >= target) { return createPlanResult(true, [], []); // already have item, no need to craft it. } else { count -= matchingItem.count; } } if (recipes.length === 0) { return createPlanResult(true, [item], []); } if (seen.has(id)) { return createPlanResult(false, [item], []); } seen.set(id, item); recipeWanted = findBestCraftableRecipe(availableItems, recipes, count, itemMatchesIngredient); if (recipeWanted == null) { // since no recipes exist with all items available, search for the recipe with the most amount of items available inline const scoredRecipes = recipes .map((recipe) => { const ingredients = recipeInputs(recipe).map((e) => ({ id: e.id, count: -e.count })); const score = ingredients.filter((ingredient) => canProvideIngredientDirectly(ingredient, availableItems)).length; return { recipe, score }; }) .sort((a, b) => b.score - a.score); const mostAmt = (_d = (_c = scoredRecipes[0]) === null || _c === void 0 ? void 0 : _c.score) !== null && _d !== void 0 ? _d : 0; // store current amount of items available to be crafted let craftedCount = 0; let bestPartialCount = 0; let bestPartialRecipes = []; outer: for (const scoredRecipe of scoredRecipes) { if (scoredRecipe.score !== mostAmt) continue; // Candidate planning mutates inventory counts, so isolate attempts until one fully succeeds. const candidateItems = cloneAvailableItems(availableItems); const candidateOpts = Object.assign(Object.assign({}, opts), { availableItems: candidateItems }); const currentItems = candidateItems; const candidateSeen = new Map(seen); const candidateRecipes = []; const recipe = scoredRecipe.recipe; const recipeIngredients = recipeInputs(recipe); // all items that need to be crafted to craft this recipe let ingredients = recipeIngredients.filter((i) => availableItemCount(currentItems, i.id, itemMatchesIngredient) < -i.count); // store all results for crafting attempts on all ingredients of current recipe const results = []; const found = ingredients.find((e) => e.id === id); if (found != null) ingredients = [found]; // do craft on all ingredients of current recipe inner: for (const ing of ingredients) { const data = _newCraft({ id: ing.id, count: -ing.count }, candidateOpts, new Map(candidateSeen)); if (!isCompleteStatus(data.status)) continue inner; results.push(data); candidateRecipes.push(...data.recipesToDo); applyRecipeResults(currentItems, data.recipesToDo, itemMatchesIngredient); } // if we successfully crafted all ingredients, we can craft this recipe if (results.length === ingredients.length) { // with our available items properly managed now, we can do the standard crafting option. let test; let attemptCount = count - craftedCount; tester: for (; attemptCount > 0; attemptCount--) { const recipeApplications = Math.ceil(attemptCount / recipe.result.count); const attemptItems = cloneAvailableItems(currentItems); const attemptRecipes = [...candidateRecipes]; const attemptOpts = Object.assign(Object.assign({}, candidateOpts), { availableItems: attemptItems }); const parentInputs = recipeInputs(recipe); let pass = 0; while (!canCraftRecipe(attemptItems, recipe, recipeApplications, itemMatchesIngredient)) { let madeProgress = false; pass++; if (pass > parentInputs.length * (recipeApplications + 2)) continue tester; for (const input of parentInputs) { const required = -input.count * recipeApplications; const available = availableItemCount(attemptItems, input.id, itemMatchesIngredient); const deficit = required - available; if (deficit <= 0) continue; const deficitItems = reserveItem(attemptItems, input.id, available, itemMatchesIngredient); const deficitOpts = Object.assign(Object.assign({}, attemptOpts), { availableItems: deficitItems }); const data = _newCraft({ id: input.id, count: deficit }, deficitOpts, new Map(candidateSeen), deficit); if (!isCompleteStatus(data.status) || data.recipesToDo.length === 0) continue tester; attemptRecipes.push(...data.recipesToDo); replaceItems(attemptItems, deficitItems); if (available > 0) addItem(attemptItems, { id: input.id, count: available }); applyRecipeResults(attemptItems, data.recipesToDo, itemMatchesIngredient); madeProgress = true; } if (!madeProgress) continue tester; } replaceItems(currentItems, attemptItems); candidateRecipes.splice(0, candidateRecipes.length, ...attemptRecipes); test = { recipesToDo: [{ recipeApplications, recipe }] }; craftedCount += attemptCount; break tester; } if (test === undefined) continue outer; candidateRecipes.push(...test.recipesToDo); applyRecipeResults(currentItems, test.recipesToDo, itemMatchesIngredient); if (craftedCount > bestPartialCount) { bestPartialCount = craftedCount; bestPartialRecipes = [...candidateRecipes]; } if (craftedCount !== count) { if (multipleRecipes && craftedCount > 0) { const remainingSeen = new Map(candidateSeen); remainingSeen.delete(id); const remainingItems = cloneAvailableItems(currentItems).filter((item) => item.id !== id); const data = _newCraft({ id, count: count - craftedCount }, Object.assign(Object.assign({}, opts), { availableItems: remainingItems }), remainingSeen, count - craftedCount); if (isCompleteStatus(data.status)) { return createPlanResult(true, ret0.concat(data.itemsRequiredBase), candidateRecipes.concat(data.recipesToDo)); } } continue outer; } return createPlanResult(true, ret0, candidateRecipes); } } // TODO can implement partial completion of recipes here. const hasNoRecipes = recipes.length === 0; const weHaveItem = availableItems.find((e) => e.id === id && e.count >= count); if (hasNoRecipes && weHaveItem != null) { return createPlanResult(true, [], []); } else { if (bestPartialCount > 0) { const partialRequirements = getPartialRequirements(id, count - bestPartialCount, availableItems, bestPartialRecipes, opts); return createPlanResult(false, partialRequirements.itemsRequiredBase, bestPartialRecipes, undefined, partialRequirements.itemsRequiredImmediate, partialRequirements.itemsRemaining); } if (!multipleRecipes || (hasNoRecipes && weHaveItem == null)) { const new1 = { id, count: count - craftedCount }; return createPlanResult(false, [new1], []); } else { const data = _newCraft({ id, count: count - craftedCount }, opts, seen, target); return createPlanResult(isCompleteStatus(data.status), ret0.concat(data.itemsRequiredBase), ret1.concat(data.recipesToDo)); } } } } else { // TODO : should be replaced by smelting recipe data const found = recipes.find((r) => r.result.count > 1); recipeWanted = found !== null && found !== void 0 ? found : recipes[0]; if (recipes.length === 0) { return createPlanResult(true, [item], []); } if (seen.has(id)) { if (!includeRecursion) { return createPlanResult(true, [item], []); } return createPlanResult(true, [item], []); } seen.set(id, item); } const recipeApplications = Math.ceil(count / recipeWanted.result.count); const items = recipeWanted.delta.slice(0, -1).map((e) => ({ id: e.id, count: -recipeApplications * e.count })); const ret = items.reduce((acc, item) => { const r = _newCraft(item, opts, seen); return { status: deriveStatus(isCompleteStatus(acc.status) && isCompleteStatus(r.status), []), itemsRequiredBase: acc.itemsRequiredBase.concat(r.itemsRequiredBase), itemsRequiredImmediate: [], itemsRemaining: [], itemsCreated: [], recipesToDo: r.recipesToDo.concat(acc.recipesToDo) }; }, { status: 'complete', itemsRequiredBase: [], itemsRequiredImmediate: [], itemsRemaining: [], itemsCreated: [], recipesToDo: [{ recipeApplications, recipe: recipeWanted }] }); seen.clear(); return createPlanResult(isCompleteStatus(ret.status), ret.itemsRequiredBase, ret.recipesToDo); } function newCraft(item, opts = {}) { const seen = new Map(); // rough, but easy way to patch out items that are already available. // can clean up later. if (opts.availableItems != null) { if (opts.careAboutExisting !== true) { const found = opts.availableItems.filter((e) => e.id === item.id); for (const f of found) { opts.availableItems.splice(opts.availableItems.indexOf(f), 1); } } // normalize items, bug pointed out by Vakore. const seen = new Set(); for (const item of opts.availableItems) { if (seen.has(item.id)) { opts.availableItems.splice(opts.availableItems.indexOf(item), 1); const existing = opts.availableItems.find((e) => e.id === item.id); if (existing != null) existing.count += item.count; } seen.add(item.id); } } const ret = _newCraft(item, opts, seen); const availableItems = opts.availableItems; const ret1 = ret; // due to multiple recipes, preserve order of items required. if (availableItems !== undefined) { if (!isConcretePlanForAvailableItems(ret, availableItems, itemMatchesIngredient)) { return newCraft(item, Object.assign(Object.assign({}, opts), { availableItems: undefined })); } ret1.requiresCraftingTable = ret.recipesToDo.some((r) => r.recipe.requiresTable); return ret1; } ret.itemsRequiredBase = []; ret.itemsRequiredImmediate = []; ret.itemsRemaining = []; const map = {}; if (opts.includeRecursion !== true) { hey: while (ret.recipesToDo.length > 0) { // remove single-level loops let change = 0; inner: for (const res1 of ret.recipesToDo) { const res = res1.recipe.result; const res2 = res1.recipe.delta.slice(0, 1); const found = ret.recipesToDo.find((r1) => r1 !== res1 && r1.recipe.delta.length === res1.recipe.delta.length && !(r1.recipe.delta.find((i) => i.id !== r1.recipe.result.id && i.id === res.id) == null) && res2.find((i) => i.id === r1.recipe.result.id)); if (found == null) continue inner; const consumerIdx = ret.recipesToDo.indexOf(res1); ret.recipesToDo.splice(consumerIdx, 1); if (ret.recipesToDo.length <= 1) break hey; const producerIdx = ret.recipesToDo.indexOf(found); ret.recipesToDo.splice(producerIdx, 1); change++; } if (change === 0) break hey; } } else { hey: while (ret.recipesToDo.length > 0) { // remove single-level loops let change = 0; inner: for (const res1 of ret.recipesToDo) { const res = res1.recipe.result; const res2 = res1.recipe.delta.slice(0, 1); const found = ret.recipesToDo.find((r1) => r1 !== res1 && r1.recipe.delta.length === res1.recipe.delta.length && !(r1.recipe.delta.find((i) => i.id !== r1.recipe.result.id && i.id === res.id) == null) && res2.find((i) => i.id === r1.recipe.result.id)); // console.log("found loop", !!res1, !!res, found); if (found == null) continue inner; const consumerIdx = ret.recipesToDo.indexOf(res1); ret.recipesToDo.splice(consumerIdx, 1); change++; if (ret.recipesToDo.length === 1) break hey; } if (change === 0) break hey; } } // console.log(ret.recipesToDo.map((r) => r.recipe.delta.map((i) => [i.count, itemsMap[i.id].name]))); for (let i = 0; i < ret.recipesToDo.length; i++) { const res = ret.recipesToDo[i]; const recipe = res.recipe; const recipeApplications = res.recipeApplications; const delta = recipe.delta; for (let j = 0; j < delta.length; j++) { const ing = delta[j]; const count = ing.count * recipeApplications; const val = map[ing.id]; const nan = isNaN(val); if (nan) map[ing.id] = count; else map[ing.id] += count; } } if (ret.recipesToDo.length > 1) { for (let idx = 0; idx < ret.recipesToDo.length; idx++) { const res = ret.recipesToDo[idx]; if (res.recipe.result.id === item.id) continue; const potentialShift = res.recipe.delta.slice(0, -1).some((i) => map[i.id] < 0); if (!potentialShift) continue; const valid = res.recipe.delta.reduce((acc, ing) => (map[ing.id] < 0 ? true : map[ing.id] - ing.count >= 0 && acc), true); if (valid) { for (const ing of res.recipe.delta) { map[ing.id] -= ing.count; } // removed this so users can know when intermediate items are crafted. // uncomment to remove blanks. // for (const ing of res.recipe.delta) { // const val = map[ing.id]; // if (val === 0) delete map[ing.id]; // } ret.recipesToDo.splice(idx, 1); } } } for (const [key, val] of Object.entries(map)) { const key1 = Number(key); if (key1 === item.id) continue; if (val >= 0) continue; const required = { id: key1, count: -val }; ret.itemsRequiredBase.push(required); ret.itemsRequiredImmediate.push(required); } ret1.requiresCraftingTable = ret.recipesToDo.some((r) => r.recipe.requiresTable); return ret1; } return newCraft; } async function buildStatic(Recipe) { return _build(Recipe); }