Module:WorkshopsRecipesData

--- --- Module for compiling recipe information from wiki data sources. --- Restructures the flat data tables that are produced from CsvUtils to make --- them more conducive to Lua methods that need to display the information. --- --- The standard way of using this module is a call like the following: --- --- gradeStars = WorkshopsRecipesData.getRecipeGradeByID(recipeID). --- --- This will return the number of stars of efficiency for the recipe. It is --- preferable to call getter methods with the ID of the recipe rather than --- retrieving the entire record for the recipe, so that your code stays --- protected from variations in this module. The only way to look up recipes --- is with their ID, so the getter methods all require an ID, which you can --- get from workshops, for example, or from retrieving either of the lists of --- recipe IDs from this module: --- --- list = WorkshopsRecipesData.getAllRecipeIDsForProductID(productID) --- list = WorkshopsRecipesData.getAllRecipeIDsWithIngredientID(ingredientID) --- --- Then you can use those IDs with these getter methods: --- --- * gradeStars = WorkshopsRecipesData.getRecipeGradeByID(recipeID) --- * timeSeconds = WorkshopsRecipesData.getRecipeProductionTimeByID(recipeID) --- * productID, stackSize = WorkshopsRecipesData.getRecipeProductByID(recipeID) --- * optionID, stackSize = WorkshopsRecipesData.getRecipeOptionByID(recipeID, ingredientIndex, optionIndex) --- --- There are three ways of getting the ingredients, based on the needs of your --- code. The most specific way that does not return a table is the preferred --- way of retrieving the data, because it is the most protected from changes --- in this module. That is the way mentioned in the previous list, to get the --- specified option at the specified index, great for i,j loops. It returns --- nil when there are no more options. --- --- The other ways return subsets of the data table (see below). These are --- usable with ipairs iterators: --- --- * ingredientsTable = WorkshopsRecipesData.getAllRecipeIngredientsByID(recipeID) --- * optionsTable = WorkshopsRecipesData.getAllRecipeIngredientOptionsByID(recipeID, ingredientIndex) --- --- As a last resort, you may use methods that begin with "getAll..." that --- return tables of data (that is, other than the ones that get lists of --- recipeIDs), but as mentioned they are less preferable, because the internal --- workings of this module may change, and these tables may suddenly change --- their structure, ruining your code. When in doubt, take the extra time to --- write code that uses one of the specific getter methods (that begin with --- "getRecipe..."). --- --- The data table for these workshop recipes has the following structure: --- --- recipesTable = { ---		["recipe1_ID"] = { --- 		["id"] = "recipe1_ID", --- 		["gradeID"] = "Grade2", --- 		["productionTime"] = 99, in seconds --- 		["productID"] = "product1_ID" ---			["productStackSize"] = 99 --- 		["ingredients"] = { --- 			[1] = { -- options for first ingredient --- 				[1] = { ["stackSize"] == 99, ["goodID"] = "good1_ID" }, --- 				[2] = { ["stackSize"] == 99, ["goodID"] = "good2_ID" }, --- 				[3] = { ["stackSize"] == 99, ["goodID"] = "good3_ID" }, --- 				[4] = { ["stackSize"] == 99, ["goodID"] = "good4_ID" }, --- 				[5] = { ["stackSize"] == 99, ["goodID"] = "good5_ID" }, --- 				[6] = { ... } or missing if fewer --- 			}, --- 			[2] = { -- options for second ingredient --- 				[1] = { ["stackSize"] == 99, ["goodID"] = "good1_ID" } --- 				-- will still have an inside table with [1] for non-optional ingredients like wood for planks --- 			}, --- 			[4] = { ... }, or missing if fewer --- 		} ---		}, ---		["recipe2_ID"] = { ---			... ---		}, --- 	["recipe3_ID"] = { --- 		... --- 	}, ---		... --- } --- --- @module WorkshopsRecipesData local WorkshopsRecipesData = {}

local CsvUtils = require("Module:CsvUtils")

--region Private member variables

--- Main data tables, like this: table[recipeID] = table containing recipe --- data local recipesTable

--- Lookup map. Built once and reused on subsequent calls within this session, --- like this: table[productGoodID] = { recipeID, recipeID, ... } local mapProductIDsToRecipeIDs --- Lookup map. Built once and reused on subsequent calls within this session, --- like this: table[ingredientGoodID] = { recipeID, recipeID, ... } local mapIngredientIDsToRecipeIDs

--endregion

--region Private constants

local DATA_TEMPLATE_NAME = "Template:Workshops_Recipes_csv"

local INDEX_ID = "id" local INDEX_GRADE = "gradeID" local INDEX_PRODUCTION_TIME = "productionTime" local INDEX_PRODUCT_ID = "productID" local INDEX_PRODUCT_STACK_SIZE = "productStackSize" local INDEX_INGREDIENTS = "ingredients"

local INDEX_INGREDIENTS_STACK_SIZE = "stackSize" local INDEX_INGREDIENTS_GOOD_ID = "goodID"

local PATTERN_SPLIT_STACK_AND_ID = "(%d+)%s([%[%]%s%a]+)" local PATTERN_CAPTURE_END_NUMBER = "Grade(%d)"

--endregion

--region Private methods

--- --- Creates a subtable of subtables containing the ingredient options for the --- provided recipe. --- --- @param originalRecipe table recipe from which to extract ingredient information --- @param recipesHeaderLookup table header lookup built from the CSV data --- @return table subtable with the ingredients options local function makeIngredientsSubtable(recipeID, originalRecipe, recipesHeaderLookup)

-- A few constants we'll need only within this function. local MAX_INGREDIENTS = 3 local MAX_INGREDIENT_OPTIONS = 6 local LOOKUP_INGREDIENT_BASE_STRING = "ingredient" local LOOKUP_OPTION_BASE_STRING = "Good"

if not mapIngredientIDsToRecipeIDs then mapIngredientIDsToRecipeIDs = {} end

-- The subtable of all ingredients and all their options. local ingredients = {} for i = 1, MAX_INGREDIENTS do

-- A subtable for the options for this ingredient. local optionsList = {} for j = 1, MAX_INGREDIENT_OPTIONS do

-- Finds the int index for each header like "ingredient1option1". local originalIndex = recipesHeaderLookup[LOOKUP_INGREDIENT_BASE_STRING .. i .. LOOKUP_OPTION_BASE_STRING .. j]

-- Once it gets to a blank, can advance to the next option or -- next ingredient. local originalOption = originalRecipe[originalIndex]

if originalOption ~= "" then -- Split the option string into number and ID. local optionStackSize, optionGoodID = originalOption:match(PATTERN_SPLIT_STACK_AND_ID) local newOption = { [INDEX_INGREDIENTS_STACK_SIZE] = optionStackSize, [INDEX_INGREDIENTS_GOOD_ID] = optionGoodID }				table.insert(optionsList, newOption)

-- Store it to the lookup map too. if not mapIngredientIDsToRecipeIDs[optionGoodID] then mapIngredientIDsToRecipeIDs[optionGoodID] = {} end table.insert(mapIngredientIDsToRecipeIDs[optionGoodID], recipeID) end end

-- Skip any blank lists. if #optionsList > 0 then table.insert(ingredients, optionsList) end end

return ingredients end

--- --- Transforms the originalRecipesTable returned from CSV processing to be more --- conducive to member functions looking up data. Converts text strings --- into subtables. Converts header row into field keys on every record and --- stores records by id rather than arbitrary integer. --- --- @param originalRecipesTable table of CSV-based data, with header row, data rows --- @param recipesHeaderLookup table lookup table of headers to get indexes --- @return table better structured with IDs as keys local function restructureRecipesTable(originalRecipesTable, recipesHeaderLookup)

-- A few constants we'll need only within this function. local DATA_ROWS = 2 local INDEX_ORIGINAL_ID = 1 local INDEX_ORIGINAL_GRADE = 2 local INDEX_ORIGINAL_PRODUCTION_TIME = 3 local INDEX_ORIGINAL_PRODUCT = 4

mapProductIDsToRecipeIDs = {}

local newRecipeTable = {} for _, originalRecipe in ipairs(originalRecipesTable[DATA_ROWS]) do

local newRecipe = {}

newRecipe[INDEX_ID] = originalRecipe[INDEX_ORIGINAL_ID] newRecipe[INDEX_GRADE] = originalRecipe[INDEX_ORIGINAL_GRADE]:match(PATTERN_CAPTURE_END_NUMBER) newRecipe[INDEX_PRODUCTION_TIME] = originalRecipe[INDEX_ORIGINAL_PRODUCTION_TIME]

-- Split the product string into number and ID. local productStackSize, productID = originalRecipe[INDEX_ORIGINAL_PRODUCT]:match(PATTERN_SPLIT_STACK_AND_ID) newRecipe[INDEX_PRODUCT_STACK_SIZE] = productStackSize newRecipe[INDEX_PRODUCT_ID] = productID

newRecipe[INDEX_INGREDIENTS] = makeIngredientsSubtable(newRecipe[INDEX_ID], originalRecipe, recipesHeaderLookup)

-- Populate the lookup map of product IDs to the recipes that make them. if not mapProductIDsToRecipeIDs[productID] then mapProductIDsToRecipeIDs[productID] = {} end table.insert(mapProductIDsToRecipeIDs[productID], newRecipe[INDEX_ID])

newRecipeTable[ newRecipe[INDEX_ID] ] = newRecipe end

return newRecipeTable end

--- --- Data loader function that uses the utility module and restructures the data --- to be easier to access for invoking methods and later method calls. This --- method is automatically called by all public member functions if the main --- data table has not yet been populated in the current session. local function loadData

-- Utility module retrieves the data as basic, flat lua tables. local originalRecipesTable, recipesHeaderLookup = CsvUtils.extractTables(DATA_TEMPLATE_NAME)

-- Now restructure to be more conducive. recipesTable = restructureRecipesTable(originalRecipesTable, recipesHeaderLookup) end

--endregion

--region Public methods

--- --- Returns the whole table of data for the specified recipe. Instead of this, --- you should probably be calling the individual getter methods. --- --- Throws an error if called with a nil or empty string. Returns nil if the --- specified recipe cannot be found. --- ---@param recipeID string the ID of the recipe ---@return table containing the data for the specified recipe, or nil if not found function WorkshopsRecipesData.getAllDataForRecipeByID(recipeID)

-- At runtime, this should never be nil or empty. if not recipeID or recipeID == "" then error("Parameter is nil or empty for recipe ID.") end

if not recipesTable then loadData end

return recipesTable[recipeID] end

--- --- Retrieve all IDs for recipes that result in the specified product. --- --- @param productID string the ID of the product (good) --- @return table the recipes that result in the product function WorkshopsRecipesData.getAllRecipeIDsForProductID(productID)

-- At runtime, this should never be nil or empty. if not productID or productID == "" then error("Parameter is nil or empty for product ID.") end

if not recipesTable then loadData end

return mapProductIDsToRecipeIDs[productID] end

--- --- Retrieves all IDs for recipes for which the specified good is an --- ingredient or optional ingredient. --- --- @param ingredientID string the ID of the ingredient (good) --- @return table the recipes that could use that ingredient function WorkshopsRecipesData.getAllRecipeIDsWithIngredientID(ingredientID)

-- At runtime, this should never be nil or empty. if not ingredientID or ingredientID == "" then error("Parameter is nil or empty for ingredient ID.") end

if not recipesTable then loadData end

return mapIngredientIDsToRecipeIDs[ingredientID] end

--- --- Retrieves the grade code for the recipe specified by its ID. --- --- Returns nil if the recipe ID was not found. --- ---@param recipeID string the ID of the recipe ---@return string the code of the grade. function WorkshopsRecipesData.getRecipeGradeByID(recipeID)

local recipe = WorkshopsRecipesData.getAllDataForRecipeByID(recipeID)

if not recipe then return nil end

return recipe[INDEX_GRADE] end

--- --- Retrieves the product time for the recipe specified by its ID. --- --- Returns nil if the recipe ID was not found. --- ---@param recipeID string the ID of the recipe ---@return number the production time, in seconds function WorkshopsRecipesData.getRecipeProductionTimeByID(recipeID)

local recipe = WorkshopsRecipesData.getAllDataForRecipeByID(recipeID)

if not recipe then return nil end

return recipe[INDEX_PRODUCTION_TIME] end

--- --- Retrieves the product and stack size for the recipe specified by its ID. --- --- Returns nil if the recipe ID was not found. --- ---@param recipeID string the ID of the recipe ---@return string the ID of the good produced by the recipe ---@return number the stack size produced function WorkshopsRecipesData.getRecipeProductByID(recipeID)

local recipe = WorkshopsRecipesData.getAllDataForRecipeByID(recipeID)

if not recipe then return nil end

return recipe[INDEX_PRODUCT_ID], recipe[INDEX_PRODUCT_STACK_SIZE] end

--- --- Retrieves the subtable of data for ingredients for the recipe specified by --- its ID. --- --- Returns nil if the recipe ID was not found. --- ---@param recipeID string the ID of the recipe ---@return table of ingredients, options, stack sizes, and good IDs function WorkshopsRecipesData.getAllRecipeIngredientsByID(recipeID)

local recipe = WorkshopsRecipesData.getAllDataForRecipeByID(recipeID)

if not recipe then return nil end

return recipe[INDEX_INGREDIENTS] end

--- --- Retrieves the specified subtable of ingredients the recipe specified by --- its ID. --- --- Returns nil if the recipe ID was not found or if there is no ingredient at --- the specified index --- ---@param recipeID string the ID of the recipe ---@param ingredientIndex number which ingredient to retrieve ---@return table of options for the specified ingredient, with stack sizes and good IDs, or nil if none function WorkshopsRecipesData.getAllRecipeIngredientOptionsByID(recipeID, ingredientIndex)

local recipe = WorkshopsRecipesData.getAllDataForRecipeByID(recipeID)

if not recipe then return nil end

return recipe[INDEX_INGREDIENTS][ingredientIndex] end

--- --- Retrieves the specified option for the recipe specified by its ID. --- --- Returns nil if the recipe ID was not found. --- ---@param recipeID string the ID of the recipe ---@param ingredientIndex number which ingredient to retrieve ---@param optionIndex number which option to retrieve ---@return string the ID of the good that is the specified option, or nil if none ---@return number the stack size of that good, or nil if none function WorkshopsRecipesData.getRecipeOptionByID(recipeID, ingredientIndex, optionIndex)

local recipe = WorkshopsRecipesData.getAllDataForRecipeByID(recipeID)

if not recipe then return nil end

local ingredient = WorkshopsRecipesData.getAllRecipeIngredientOptionsByID(recipeID, ingredientIndex) if not ingredient then return nil end

local option = ingredient[optionIndex] if not option then return nil end

return option[INDEX_INGREDIENTS_GOOD_ID], option[INDEX_INGREDIENTS_STACK_SIZE] end

--endregion

return WorkshopsRecipesData