mineflayer-crafting-util
Version:
A plugin to simplify crafting recipes.
373 lines (372 loc) • 19.3 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports._build = _build;
exports.injectBot = injectBot;
exports.buildStatic = buildStatic;
const gettableItems = [263, 264, 265, 266, 296, 331, 341, 388]; // TODO : should be replaced by smelting recipe data
function _build(Recipe) {
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) => e.id === id && e.count >= target);
if (matchingItem != null) {
if (matchingItem.count >= target) {
return { success: true, itemsRequired: [], recipesToDo: [] }; // already have item, no need to craft it.
}
else {
count -= matchingItem.count;
}
}
if (recipes.length === 0 || gettableItems.includes(id)) {
return { success: true, itemsRequired: [item], recipesToDo: [] };
}
if (seen.has(id)) {
return { success: false, itemsRequired: [item], recipesToDo: [] };
}
seen.set(id, item);
recipeWanted = recipes.find((r) => {
if (r.ingredients != null)
return r.ingredients.every(e => { var _a, _b; return ((_b = (_a = availableItems.find(i => i.id === e.id)) === null || _a === void 0 ? void 0 : _a.count) !== null && _b !== void 0 ? _b : 0) >= -e.count; });
else
return r.delta.slice(0, -1).every((e) => { var _a, _b; return ((_b = (_a = availableItems.find((i) => i.id === e.id)) === null || _a === void 0 ? void 0 : _a.count) !== null && _b !== void 0 ? _b : 0) >= -e.count; });
});
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 delta = recipe.delta.slice(0, -1).map((e) => ({ id: e.id, count: -e.count }));
const score = availableItems.filter((have) => delta.findIndex((wanted) => wanted.id === have.id && wanted.count <= have.count) !== -1).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;
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 = availableItems.map((item) => (Object.assign({}, item)));
const candidateOpts = Object.assign(Object.assign({}, opts), { availableItems: candidateItems });
const currentItems = candidateItems;
const candidateSeen = new Map(seen);
const candidateRecipes = [];
const recipe = scoredRecipe.recipe;
const ingredien = recipe.delta.slice(0, -1);
// all items that need to be crafted to craft this recipe
let ingredients = ingredien.filter((i) => currentItems.find((e) => e.id === i.id && e.count >= -i.count) === undefined);
// 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, candidateSeen);
if (!data.success)
continue inner;
results.push(data);
candidateRecipes.push(...data.recipesToDo);
for (const item1 of data.recipesToDo) {
for (let j = 0; j < item1.recipe.delta.length; j++) {
const item = item1.recipe.delta[j];
const index = currentItems.findIndex((e) => e.id === item.id);
if (index !== -1) {
currentItems[index].count += item.count * item1.recipeApplications;
}
else {
currentItems.push({ id: item.id, count: item.count * item1.recipeApplications });
}
}
}
}
// 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 test1 = _newCraft({ id, count: attemptCount }, candidateOpts, candidateSeen, target);
if (test1.success) {
test = test1;
craftedCount += attemptCount;
break tester;
}
}
if (test === undefined)
continue outer;
candidateRecipes.push(...test.recipesToDo);
for (const toDo of test.recipesToDo) {
for (const ing of toDo.recipe.delta) {
const index = currentItems.findIndex((e) => e.id === ing.id);
// const num = (currentItems[index]?.count ?? 0) + ing.count * toDo.recipeApplications;
// if (num < 0) { // this should never happen, but just in case.
// return { success: false, itemsRequired: [item], recipesToDo: [] };
// }
if (index !== -1) {
currentItems[index].count += ing.count * toDo.recipeApplications;
}
else {
currentItems.push({ id: ing.id, count: ing.count * toDo.recipeApplications });
}
}
}
if (craftedCount !== count) {
continue outer;
}
return {
success: true,
itemsRequired: ret0,
recipesToDo: candidateRecipes
};
}
}
// TODO can implement partial completion of recipes here.
const hasNoRecipes = recipes.length === 0 || gettableItems.includes(id);
const weHaveItem = availableItems.find((e) => e.id === id && e.count >= count);
if (hasNoRecipes && weHaveItem != null) {
return { success: true, itemsRequired: [], recipesToDo: [] };
}
else {
if (!multipleRecipes || (hasNoRecipes && weHaveItem == null)) {
const new1 = { id, count: count - craftedCount };
return { success: false, itemsRequired: [new1], recipesToDo: [] };
}
else {
const data = _newCraft({ id, count: count - craftedCount }, opts, seen, target);
return {
success: data.success,
itemsRequired: ret0.concat(data.itemsRequired),
recipesToDo: 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 || gettableItems.includes(id)) {
return { success: true, itemsRequired: [item], recipesToDo: [] };
}
if (seen.has(id)) {
if (!includeRecursion) {
return { success: true, itemsRequired: [item], recipesToDo: [] };
}
return { success: true, itemsRequired: [item], recipesToDo: [] };
}
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 {
success: acc.success && r.success,
itemsRequired: acc.itemsRequired.concat(r.itemsRequired),
recipesToDo: r.recipesToDo.concat(acc.recipesToDo)
};
}, { success: true, itemsRequired: [], recipesToDo: [{ recipeApplications, recipe: recipeWanted }] });
seen.clear();
return ret;
}
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) {
ret1.requiresCraftingTable = ret.recipesToDo.some((r) => r.recipe.requiresTable);
return ret1;
}
ret.itemsRequired = [];
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;
ret.itemsRequired.push({ id: key1, count: val >= 0 ? 0 : -val });
}
ret1.requiresCraftingTable = ret.recipesToDo.some((r) => r.recipe.requiresTable);
return ret1;
}
return newCraft;
}
async function injectBot(bot, botoptions) {
// @ts-expect-error
const Recipe = (await Promise.resolve().then(() => __importStar(require('prismarine-recipe')))).default(bot.registry).Recipe;
const newCraft = _build(Recipe);
bot.planCraft = newCraft;
function craftWithInventory(wantedItem) {
const items = bot.inventory.slots.filter(i => !(i == null)).map(i => { return { id: i.type, count: i.count }; });
return newCraft(wantedItem, {
availableItems: items,
careAboutExisting: false,
includeRecursion: true,
multipleRecipes: true
});
}
bot.planCraftInventory = craftWithInventory;
}
async function buildStatic(registry) {
// @ts-expect-error
const Recipe = (await Promise.resolve().then(() => __importStar(require('prismarine-recipe')))).default(registry).Recipe;
return _build(Recipe);
}