Module:ServicesData

From Against the Storm Official Wiki
Revision as of 21:22, 31 October 2024 by Aeredor (talk | contribs) (Adding a validation method to this otherwise deprecated model until I figure out another solution)

Documentation for this module may be created at Module:ServicesData/doc

---
--- Module for compiling services recipes 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:
---
--- goodID = ServicesRecipesData.getServiceGoodID(serviceName)
---
--- This will return the ID of the good that is used to satisfy the service
--- need. It is preferable to call getter methods with the ID of the service
--- rather than retrieving the entire record for the recipe, so that your code
--- stays protected from variations in this module.
---
--- * serviceID = ServicesRecipesData.getServiceRecipeID(serviceName)
--- * gradeStars = ServicesRecipesData.getServiceRecipeGrade(serviceName)
--- * goodID = ServicesRecipesData.getServiceGoodID(serviceName)
---
--- The most specific way of retrieving data 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.
---
--- 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 services recipes has the following structure:
---
--- recipesTable = {
---		["recipe1_ID"] = {
--- 		["id"] = "recipe1_ID",
--- 		["gradeID"] = 1,
--- 		["serviceNeed"] = "Education"
---			["serviceGoods"] = {
--- 			[1] = {
--- 				["goodID"] = "good1_ID",
--- 				["stackSize"] = 1
--- 			},
--- 			[2] = { ... } or missing if fewer
--- 		}
---		},
---		["recipe2_ID"] = {
---			...
---		},
--- 	["recipe3_ID"] = {
--- 		...
--- 	},
---		...
--- }
---
--- @module ServicesRecipesData
local ServicesRecipesData = {}



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[serviceName] = { recipeID, recipeID, ... }
local mapNamesToIDs
--- Lookup map. Built once and reused on subsequent calls within this session,
--- like this:  table[goodID] = { serviceNeed1, serviceNeed2, ... }
local mapGoodIDsToRecipeIDs

--endregion



--region Private constants

local DATA_TEMPLATE_NAME = "Template:Institutions_Recipes_csv"

local INDEX_ID = "id"
local INDEX_GRADE = "gradeID"
local INDEX_SERVICE_NAME = "serviceNeed"
local INDEX_GOODS = "serviceGoods"

local INDEX_GOODS_GOOD_ID = "goodID"
local INDEX_GOODS_STACK_SIZE = "stackSize"

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

local SERVICE_ICONS = {
	["Brawling"] = "Icon_Need_Brawling.png",
	["Education"] = "Icon_Need_Education.png",
	["Leisure"] = "Icon_Need_Leisure.png",
	["Luxury"] = "Icon_Need_Luxury.png",
	["Religion"] = "Icon_Need_Religion.png",
	["Treatment"] = "Icon_Need_Treatment.png"
}

--endregion



--region Private methods

---
--- 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_SERVED_NEED = 3
	local MAX_SERVICES_GOODS = 6
	local LOOKUP_SERVICE_GOODS_BASE_STRING = "Good"

	mapNamesToIDs = {}
	mapGoodIDsToRecipeIDs = {}

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

		local newRecipe = {}

		newRecipe[INDEX_ID] = originalRecipe[INDEX_ORIGINAL_ID]
		newRecipe[INDEX_GRADE] = tonumber(originalRecipe[INDEX_ORIGINAL_GRADE]:match(PATTERN_CAPTURE_END_NUMBER))
		newRecipe[INDEX_SERVICE_NAME] = originalRecipe[INDEX_ORIGINAL_SERVED_NEED]

		local serviceGoods = {}
		for i = 1, MAX_SERVICES_GOODS do
			local goodIndex = recipesHeaderLookup[LOOKUP_SERVICE_GOODS_BASE_STRING .. i]
			local stackSize, goodID = originalRecipe[goodIndex]:match(PATTERN_SPLIT_STACK_AND_ID)

			if not goodID then
				break
			end

			local newGood = {
				[INDEX_GOODS_GOOD_ID] = goodID,
				[INDEX_GOODS_STACK_SIZE] = tonumber(stackSize)
			}
			table.insert(serviceGoods, newGood)

			-- Populate the lookup map for goods.
			if not mapGoodIDsToRecipeIDs[goodID] then
				mapGoodIDsToRecipeIDs[goodID] = {}
			end
			table.insert(mapGoodIDsToRecipeIDs[goodID], newRecipe[INDEX_ID])

		end

		newRecipe[INDEX_GOODS] = serviceGoods

		-- Populate the lookup map for services.
		if not mapNamesToIDs[ newRecipe[INDEX_SERVICE_NAME] ] then
			mapNamesToIDs[ newRecipe[INDEX_SERVICE_NAME] ] = {}
		end
		table.insert( mapNamesToIDs[ newRecipe[INDEX_SERVICE_NAME] ], 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 ServicesRecipesData.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 for the specified service.
---
--- @param serviceName string the name of the service
--- @return table the recipes that result in the product
function ServicesRecipesData.getAllRecipeIDsForServiceName(serviceName)

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

	if not recipesTable then
		loadData()
	end

	return mapNamesToIDs[serviceName]
end



---
--- Retrieves all IDs for recipes for which the specified good is necessary.
---
--- @param goodID string the ID of the ingredient (good)
--- @return table the recipes that could use that ingredient
function ServicesRecipesData.getAllRecipeIDsWithServiceGoodID(goodID)

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

	if not recipesTable then
		loadData()
	end

	return mapGoodIDsToRecipeIDs[goodID]
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 ServicesRecipesData.getRecipeGradeByID(recipeID)

	local recipe = ServicesRecipesData.getAllDataForRecipeByID(recipeID)

	if not recipe then
		return nil
	end

	return recipe[INDEX_GRADE]
end



---
--- Retrieves the service name 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
function ServicesRecipesData.getRecipeServiceNameByID(recipeID)

	local recipe = ServicesRecipesData.getAllDataForRecipeByID(recipeID)

	if not recipe then
		return nil
	end

	return recipe[INDEX_SERVICE_NAME]
end



---
function ServicesRecipesData.getRecipeServiceIconByID(recipeID)

	local recipe = ServicesRecipesData.getAllDataForRecipeByID(recipeID)

	if not recipe then
		return nil
	end

	return SERVICE_ICONS[ recipe[INDEX_SERVICE_NAME] ]
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 good IDs and stack sizes
function ServicesRecipesData.getAllRecipeServiceGoodsByID(recipeID)

	local recipe = ServicesRecipesData.getAllDataForRecipeByID(recipeID)

	if not recipe then
		return nil
	end

	return recipe[INDEX_GOODS]
end



---
--- Retrieves the number of 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 number of ingredient groups
function ServicesRecipesData.getRecipeNumberOfServiceGoodsByID(recipeID)

	local recipe = ServicesRecipesData.getAllDataForRecipeByID(recipeID)

	if not recipe then
		return nil
	end

	return #recipe[INDEX_GOODS]
end



---
--- Retrieves the specified service goods 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 goodIndex number which ingredient 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 ServicesRecipesData.getRecipeOptionByID(recipeID, goodIndex)

	local recipe = ServicesRecipesData.getAllDataForRecipeByID(recipeID)

	if not recipe then
		return nil
	end

	local goods = ServicesRecipesData.getAllRecipeServiceGoodsByID(recipeID)
	if not goods then
		return nil
	end

	local good = goods[goodIndex]

	return good[INDEX_GOODS_GOOD_ID], good[INDEX_GOODS_STACK_SIZE]
end


function ServicesRecipesData.isValidName(serviceName)

	local list = ServicesRecipesData.getAllRecipeIDsForServiceName(serviceName)
	if not list or #list < 1 then
		return false
	else
		return true
	end
end

--endregion

return ServicesRecipesData