Module:WorkshopsRecipesData
From Against the Storm Official Wiki
Documentation for this module may be created at Module:WorkshopsRecipesData/doc
--- --- 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