From e2611131343a964d5efd52154de51fc4d1af8979 Mon Sep 17 00:00:00 2001 From: MayaTheShy Date: Sun, 22 Mar 2026 18:15:40 -0400 Subject: [PATCH] feat: implement unified recipe database with learning support --- lib/recipeBook.lua | 248 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 248 insertions(+) create mode 100644 lib/recipeBook.lua diff --git a/lib/recipeBook.lua b/lib/recipeBook.lua new file mode 100644 index 0000000..97eee57 --- /dev/null +++ b/lib/recipeBook.lua @@ -0,0 +1,248 @@ +-- 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 + +return recipeBook