Module:WorkshopsRecipesData: Difference between revisions

From Against the Storm Official Wiki
(Created to replace RecipeData, so that module can be made to merge recipes from production buildings with recipes from camps, farms, service buildings, etc.)
 
(Added method getRecipeNumberOfIngredientsByID)
Line 158: Line 158:


-- Finds the int index for each header like "ingredient1option1".
-- Finds the int index for each header like "ingredient1option1".
local originalIndex = recipesHeaderLookup[LOOKUP_INGREDIENT_BASE_STRING .. i .. LOOKUP_OPTION_BASE_STRING .. j]
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
-- Once it gets to a blank, can advance to the next option or
Line 408: Line 409:


return recipe[INDEX_INGREDIENTS]
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
end



Revision as of 16:00, 23 November 2023

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 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