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 number of ingredients (or groups of options) there are 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 of ingredient groups
function WorkshopsRecipesData.getRecipeNumberOfIngredientsByID(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 number of options for the specified ingredient.
---
--- 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 number of options for the specified ingredient, or nil if no ingredient
function WorkshopsRecipesData.getRecipeNumberOfOptionsByID(recipeID, ingredientIndex)
local recipe = WorkshopsRecipesData.getAllDataForRecipeByID(recipeID)
if not recipe then
return nil
end
local ingredientTable = recipe[INDEX_INGREDIENTS][ingredientIndex]
if not ingredientTable then
return nil
end
return #ingredientTable
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


