-- lib/recipeBook.lua — Unified recipe database with learning support -- Loads built-in recipes from data files + user-learned recipes from disk. -- Compatible with Prominence II Hasturian Era modpack (or any modded setup). -- -- Usage: -- local recipeBook = dofile("lib/recipeBook.lua") -- recipeBook.init(".recipes.db") -- recipeBook.loadLegacyCrafting(dofile("data/craftable.lua")) -- recipeBook.loadLegacySmelting(dofile("data/smeltable.lua")) -- recipeBook.learnCraftingRecipe("mod:item", 4, { ... }) -- recipeBook.flush() local recipeBook = {} local recipes = { crafting = {}, -- keyed by output item name smelting = {}, -- keyed by input item name } local RECIPE_FILE = nil local dirty = false ------------------------------------------------- -- Initialization and persistence ------------------------------------------------- function recipeBook.init(recipeFile) RECIPE_FILE = recipeFile recipeBook.loadUserRecipes() end --- Load legacy crafting recipes from data/craftable.lua format. -- Does not overwrite recipes already loaded (user-learned take priority). function recipeBook.loadLegacyCrafting(craftableArray) for _, recipe in ipairs(craftableArray) do if not recipes.crafting[recipe.output] then recipes.crafting[recipe.output] = { output = recipe.output, count = recipe.count, grid = recipe.grid, source = "builtin", } end end end --- Load legacy smelting recipes from data/smeltable.lua format. -- Does not overwrite recipes already loaded (user-learned take priority). function recipeBook.loadLegacySmelting(smeltableTable) for input, recipe in pairs(smeltableTable) do if not recipes.smelting[input] then recipes.smelting[input] = { input = input, result = recipe.result, furnaces = recipe.furnaces, source = "builtin", } end end end --- Load user-learned recipes from disk. function recipeBook.loadUserRecipes() if not RECIPE_FILE or not fs.exists(RECIPE_FILE) then return end pcall(function() local f = fs.open(RECIPE_FILE, "r") local raw = f.readAll() f.close() local data = textutils.unserialise(raw) if type(data) == "table" then for output, recipe in pairs(data.crafting or {}) do recipe.source = "learned" recipe.output = output recipes.crafting[output] = recipe end for input, recipe in pairs(data.smelting or {}) do recipe.source = "learned" recipe.input = input recipes.smelting[input] = recipe end end end) end --- Save user-learned recipes to disk. function recipeBook.flush() if not dirty or not RECIPE_FILE then return end pcall(function() local uc, us = {}, {} for output, r in pairs(recipes.crafting) do if r.source == "learned" then uc[output] = { output = r.output, count = r.count, grid = r.grid } end end for input, r in pairs(recipes.smelting) do if r.source == "learned" then us[input] = { result = r.result, furnaces = r.furnaces } end end local f = fs.open(RECIPE_FILE, "w") f.write(textutils.serialise({ crafting = uc, smelting = us })) f.close() end) dirty = false end ------------------------------------------------- -- Learning / forgetting recipes ------------------------------------------------- --- Learn a new crafting recipe (or overwrite an existing one). -- @param output string — output item name (e.g. "minecraft:stick") -- @param count number — items produced per craft -- @param grid table — 9-entry grid array function recipeBook.learnCraftingRecipe(output, count, grid) recipes.crafting[output] = { output = output, count = count, grid = grid, source = "learned", } dirty = true end --- Learn a new smelting recipe. -- @param input string — input item name -- @param result string — output item name -- @param furnaces table — array of furnace type strings (default: {"minecraft:furnace"}) function recipeBook.learnSmeltingRecipe(input, result, furnaces) recipes.smelting[input] = { input = input, result = result, furnaces = furnaces or { "minecraft:furnace" }, source = "learned", } dirty = true end --- Forget a learned crafting recipe (built-in recipes cannot be forgotten). -- @return true if removed, false if recipe was built-in or not found function recipeBook.forgetCraftingRecipe(output) if recipes.crafting[output] and recipes.crafting[output].source == "learned" then recipes.crafting[output] = nil dirty = true return true end return false end --- Forget a learned smelting recipe. function recipeBook.forgetSmeltingRecipe(input) if recipes.smelting[input] and recipes.smelting[input].source == "learned" then recipes.smelting[input] = nil dirty = true return true end return false end ------------------------------------------------- -- Lookups ------------------------------------------------- --- Get a crafting recipe by output item name. function recipeBook.getCraftingRecipe(output) return recipes.crafting[output] end --- Get a smelting recipe by input item name. function recipeBook.getSmeltingRecipe(input) return recipes.smelting[input] end --- Find any recipe (crafting or smelting) that produces a given item. -- @return recipe, recipeType ("crafting" or "smelting") or nil function recipeBook.findRecipeFor(itemName) if recipes.crafting[itemName] then return recipes.crafting[itemName], "crafting" end for input, recipe in pairs(recipes.smelting) do if recipe.result == itemName then return recipe, "smelting" end end return nil end ------------------------------------------------- -- Backward-compatible accessors ------------------------------------------------- --- Get all crafting recipes as an indexed array (compat with cfg.CRAFTABLE). -- Sorted by output name. function recipeBook.getCraftingList() local list = {} for _, recipe in pairs(recipes.crafting) do table.insert(list, recipe) end table.sort(list, function(a, b) return a.output < b.output end) return list end --- Get all smelting recipes as a keyed table (compat with cfg.SMELTABLE). function recipeBook.getSmeltingTable() local result = {} for input, recipe in pairs(recipes.smelting) do result[input] = recipe end return result end ------------------------------------------------- -- Utilities ------------------------------------------------- --- Get summed ingredients for a crafting recipe. -- @return table { [itemName] = count } function recipeBook.getIngredients(recipe) local ingredients = {} if recipe and recipe.grid then for _, item in ipairs(recipe.grid) do if item then ingredients[item] = (ingredients[item] or 0) + 1 end end end return ingredients end --- Quick craftability check. function recipeBook.isCraftable(itemName) return recipes.crafting[itemName] ~= nil end --- Quick smeltability check. function recipeBook.isSmeltable(itemName) return recipes.smelting[itemName] ~= nil end --- Count total recipes. -- @return craftCount, smeltCount function recipeBook.count() local cc, sc = 0, 0 for _ in pairs(recipes.crafting) do cc = cc + 1 end for _ in pairs(recipes.smelting) do sc = sc + 1 end return cc, sc end --- Find all crafting recipes that use a given item as an ingredient. -- @param ingredientName string — the input item to search for -- @return array of recipe tables function recipeBook.findRecipesUsing(ingredientName) local results = {} for _, recipe in pairs(recipes.crafting) do if recipe.grid then for _, item in ipairs(recipe.grid) do if item == ingredientName then table.insert(results, recipe) break end end end end return results end return recipeBook