Module:RecipeController

From Against the Storm Official Wiki
Revision as of 21:33, 26 November 2023 by Aeredor (talk | contribs) (Created to work with RecipeView and data model to display recipes across workshops, services, farms, and camps)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)

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

---
--- Module for merging recipe information from data managers.
---
--- @module RecipeData
local RecipeController = {}



--region Dependencies

local RecipeView = require("Module:RecipeView")

local WorkshopsRecipesData = require("Module:WorkshopsRecipesData")
local ServicesRecipesData = require("Module:ServicesRecipesData")

local InstitutionsData = require("Module:InstitutionsData")
local WorkshopsData = require("Module:WorkshopsData")
local FarmsData = require("Module:FarmsData")
local CampsData = require("Module:CampsData")

local GoodsData = require("Module:GoodsData")

--endregion



--region Private member variables

--endregion



--region Private constants

local MAX_INGREDIENTS = 3
local INDEX_OPTION_STACK_SIZE = "stackSize"
local INDEX_OPTION_GOOD_NAME = "name"
local INDEX_OPTION_GOOD_ICON = "icon"

local TABLE_HEADER_BOTH = "Product or Service"
local TABLE_HEADER_PRODUCT = "Product"
local TABLE_HEADER_SERVICE = "Service"

--endregion



--region Private methods

---extractIngredientsNamesAndIcons
---@param recipeID string the ID of the recipe from which to extract ingredients
---@return table of ingredients and options (stack size, name, and icon)
local function extractIngredientsNamesAndIcons(recipeID)

	local ingredientsList = {}
	-- Need to convert IDs to names and icons and keep stack size.
	for i = 1, MAX_INGREDIENTS do
		for j = 1, WorkshopsRecipesData.getRecipeNumberOfOptionsByID(recipeID, i) do

			if not ingredientsList[i] then
				ingredientsList[i] = {}
			end

			local goodID, stackSize = WorkshopsRecipesData.getRecipeOptionByID(recipeID, i, j)
			ingredientsList[i][j] = {}
			ingredientsList[i][j][INDEX_OPTION_STACK_SIZE] = stackSize
			ingredientsList[i][j][INDEX_OPTION_GOOD_NAME] = GoodsData.getGoodNameByID(goodID)
			ingredientsList[i][j][INDEX_OPTION_GOOD_ICON] = GoodsData.getGoodIconByID(goodID)
		end
	end

	return ingredientsList
end



---extractServiceGoodsNamesAndIcons
---@param recipeID string the ID of the recipe from which to extract ingredients
---@return table of ingredients and options (stack size, name, and icon)
local function extractServiceGoodsNamesAndIcons(recipeID)

	local ingredientsList = {}
	-- Need to convert IDs to names and icons and keep stack size.
	for i = 1, ServicesRecipesData.getRecipeNumberOfServiceGoodsByID(recipeID) do

		ingredientsList[i] = {}

		-- Need to structure this the same way as when there are several
		-- ingredient option groups. But for services there's just one.
		local j = 1
		ingredientsList[i][j] = {}
		local goodID, stackSize = ServicesRecipesData.getRecipeOptionByID(recipeID, i)
		ingredientsList[i][j][INDEX_OPTION_STACK_SIZE] = stackSize
		ingredientsList[i][j][INDEX_OPTION_GOOD_NAME] = GoodsData.getGoodNameByID(goodID)
		ingredientsList[i][j][INDEX_OPTION_GOOD_ICON] = GoodsData.getGoodIconByID(goodID)
	end

	return ingredientsList
end




local function buildRowsForWorkshops(recipeListFromWorkshops, displayOverride, numIngredients, requiredBuilding)

	-- Build the rows of the middle of the view. If it's nil and there are no
	-- recipes, skip it by getting ipairs on an empty table.
	for _, recipeID in ipairs(recipeListFromWorkshops or {}) do

		local gradeStars = WorkshopsRecipesData.getRecipeGradeByID(recipeID)
		local productionTime = WorkshopsRecipesData.getRecipeProductionTimeByID(recipeID)
		local productID, productStackSize = WorkshopsRecipesData.getRecipeProductByID(recipeID)

		local ingredientsList = extractIngredientsNamesAndIcons(recipeID)

		local productName = GoodsData.getGoodNameByID(productID)
		local productIcon = GoodsData.getGoodIconByID(productID)

		local buildingListFromWorkshops = WorkshopsData.getWorkshopNamesWithRecipeID(recipeID)

		for _, buildingName in ipairs(buildingListFromWorkshops) do

			-- If we're screening for a building (not nil), then we have
			-- to match the current building
			if not requiredBuilding or buildingName == requiredBuilding then

				local buildingIcon = WorkshopsData.getWorkshopIcon(buildingName)

				RecipeView.addRowForRecipe(displayOverride, numIngredients,
						buildingName, buildingIcon,
						gradeStars, productionTime, ingredientsList,
						productStackSize, productName, productIcon)

			end
		end

	end
end



local function buildRowsForServices(recipeListFromServices, displayOverride, numIngredients, requiredBuilding)

	-- Same. If it's nil and there are no recipes, skip it by getting ipairs
	-- on an empty table.
	for _, recipeID in ipairs(recipeListFromServices or {}) do

		local gradeStars = ServicesRecipesData.getRecipeGradeByID(recipeID)
		local serviceName = ServicesRecipesData.getRecipeServiceNameByID(recipeID)
		local serviceIcon = ServicesRecipesData.getRecipeServiceIconByID(recipeID)

		local ingredientsList = extractServiceGoodsNamesAndIcons(recipeID)

		local buildingListFromInstitutions = InstitutionsData.getInstitutionNamesWithRecipeID(recipeID)

		local productionTime -- = nil
		local resultStackSize -- = nil

		for _, buildingName in ipairs(buildingListFromInstitutions) do

			-- If we're screening for a building (not nil), then we have
			-- to match the current building
			if not requiredBuilding or buildingName == requiredBuilding then

				local buildingIcon = InstitutionsData.getInstitutionIcon(buildingName)

				RecipeView.addRowForRecipe(displayOverride, numIngredients,
						buildingName, buildingIcon,
						gradeStars, productionTime, ingredientsList,
						resultStackSize, serviceName, serviceIcon)
			end
		end

	end
end



local function buildRowsForFarms(buildingListFromFarms, displayOverride, numIngredients, productID, requiredBuilding)

	for _, buildingName in ipairs(buildingListFromFarms or {}) do

		-- If we're screening for a building (not nil), then we have to match
		-- the current building
		if not requiredBuilding or buildingName == requiredBuilding then

			for i = 1, FarmsData.getFarmNumberOfRecipes(buildingName) do

				local goodID, stackSize = FarmsData.getFarmRecipeProduct(buildingName, i)

				-- If we're screening for a productID (not nil), then we have
				-- to match the current goodID
				if not productID or goodID == productID then

					local buildingIcon = FarmsData.getFarmIcon(buildingName)
					local gradeStars, plantingTime, harvestingTime = FarmsData.getFarmRecipeStats(buildingName, i)
					local ingredientsList = {}
					local productName = GoodsData.getGoodNameByID(goodID)
					local productIcon = GoodsData.getGoodIconByID(goodID)

					RecipeView.addRowForRecipe(displayOverride, numIngredients,
							buildingName, buildingIcon,
							gradeStars, (plantingTime + harvestingTime),
							ingredientsList, stackSize,
							productName, productIcon)
				end

			end
		end

	end
end



local function buildRowsForCamps(buildingListFromCamps, displayOverride, numIngredients, productID, requiredBuilding)

	for _, buildingName in ipairs(buildingListFromCamps or {}) do

		-- If we're screening for a building (not nil), then we have to match
		-- the current building
		if not requiredBuilding or buildingName == requiredBuilding then

			for i = 1, CampsData.getCampNumberOfRecipes(buildingName) do

				local goodID, stackSize = CampsData.getCampRecipeProduct(buildingName, i)

				-- If we're screening for a productID (not nil), then we have to
				-- match the current goodID
				if not productID or goodID == productID then

					local buildingIcon = CampsData.getCampIcon(buildingName)
					local gradeStars, gatheringTime = CampsData.getCampRecipeStats(buildingName, i)
					local ingredientsList = {}
					local productName = GoodsData.getGoodNameByID(goodID)
					local productIcon = GoodsData.getGoodIconByID(goodID)

					RecipeView.addRowForRecipe(displayOverride, numIngredients,
							buildingName, buildingIcon,
							gradeStars, gatheringTime,
							ingredientsList, stackSize,
							productName, productIcon)
				end

			end
		end

	end
end



---
--- Takes two lists and returns the intersection of the two. Slightly more
--- efficient if the second list is smaller than the first.
---
---@param list1 table a flat table (list of values)
---@param list2 table another flat table
---@return table a similarly flat table consisting only of the values that are in both provided lists
local function findIntersection(list1, list2)

	local intersection = {}

	-- Create a new mapping to efficiently check for existence in list2 by
	-- checking the same ID in the next loop.
	local setList2 = {}
	for _, value in ipairs(list2 or {}) do
		setList2[value] = true
	end

	-- Check for intersection and populate the result table.
	for _, valueToFind in ipairs(list1 or {}) do
		if setList2[valueToFind] then
			table.insert(intersection, valueToFind)
		end
	end

	return intersection
end

--endregion




function RecipeController.renderWithIngredient(ingredientName, displayOverride)

	-- Service and workshops recipes have ingredients. Camps and farms do not.
	local ingredientID = GoodsData.getGoodID(ingredientName)
	if not ingredientID then
		return "No ingredient found with that name: " .. ingredientName .. "."
	end
	local recipeListFromWorkshops = WorkshopsRecipesData.getAllRecipeIDsWithIngredientID(ingredientID)
	local recipeListFromServices = ServicesRecipesData.getAllRecipeIDsWithServiceGoodID(ingredientID)

	-- Find out what the largest number of ingredients in this table is. Need
	-- this before we start building any of the beginning and the middle so
	-- that they are all the same.
	local numIngredients  = 1
	if recipeListFromWorkshops then
		for _, recipeID in ipairs(recipeListFromWorkshops) do
			local thisNum = WorkshopsRecipesData.getRecipeNumberOfIngredientsByID(recipeID)
			if numIngredients < thisNum	then
				numIngredients = thisNum
			end
		end
	end
	if recipeListFromServices then
		for _, recipeID in ipairs(recipeListFromServices) do
			local thisNum = WorkshopsRecipesData.getRecipeNumberOfServiceGoodsByID(recipeID)
			if numIngredients < thisNum	then
				numIngredients = thisNum
			end
		end
	end

	-- Count the number of recipes in both the Workshops and Services.
	local numRecipes = 0
	local header -- = nil
	if recipeListFromWorkshops ~= nil then
		numRecipes = numRecipes + #recipeListFromWorkshops
		header = TABLE_HEADER_PRODUCT
	end
	if recipeListFromServices ~= nil then
		if numRecipes > 0 then
			header = TABLE_HEADER_BOTH
		end
		numRecipes = numRecipes + #recipeListFromServices
		header = TABLE_HEADER_SERVICE
	end

	if numRecipes == 0 then
		return "No recipes found for ingredient: " .. ingredientName .. "."
	end

	RecipeView.startViewForIngredient(ingredientName, displayOverride, numRecipes, numIngredients, header)

	buildRowsForWorkshops(recipeListFromWorkshops, displayOverride, numIngredients)

	buildRowsForServices(recipeListFromServices, displayOverride, numIngredients)

	return RecipeView.endView(ingredientName, displayOverride)
end



function RecipeController.renderWithProductAndBuilding(productName, buildingName, displayOverride)

	-- Get lists from all buildings
	local productID = GoodsData.getGoodID(productName)
	if not productID then
		return "No product found with that name: " .. productName .. "."
	end

	local recipeListFromWorkshopsRecipes = WorkshopsRecipesData.getAllRecipeIDsForProductID(productID)
	local recipeListFromWorkshops = WorkshopsData.getAllWorkshopRecipes(buildingName)
	recipeListFromWorkshops = findIntersection(recipeListFromWorkshopsRecipes, recipeListFromWorkshops)

	-- It could be the name of a service instead of the name of a product.
	local recipeListFromServicesRecipes = ServicesRecipesData.getAllRecipeIDsWithServiceGoodID(productName)
	local recipeListFromServices = InstitutionsData.getAllInstitutionRecipes(buildingName)
	recipeListFromServices = findIntersection(recipeListFromServicesRecipes, recipeListFromServices)

	-- These result in a list of buildings, not recipes.
	local buildingListFromCamps = CampsData.getCampNamesWithRecipeProductID(productID)
	buildingListFromCamps = findIntersection(buildingListFromCamps, {buildingName} )

	local buildingListFromFarms = FarmsData.getFarmNamesWithRecipeProductID(productID)
	buildingListFromFarms = findIntersection(buildingListFromFarms, {buildingName})

	-- Find out what the largest number of ingredients in this table is. Need
	-- this before we start building any of the beginning and the middle so
	-- that they are all the same.
	local numIngredients  = 1
	if recipeListFromWorkshops then
		for _, recipeID in ipairs(recipeListFromWorkshops) do
			local thisNum = WorkshopsRecipesData.getRecipeNumberOfIngredientsByID(recipeID)
			if numIngredients < thisNum	then
				numIngredients = thisNum
			end
		end
	end
	if recipeListFromServices then
		for _, recipeID in ipairs(recipeListFromServices) do
			local thisNum = WorkshopsRecipesData.getRecipeNumberOfServiceGoodsByID(recipeID)
			if numIngredients < thisNum	then
				numIngredients = thisNum
			end
		end
	end

	-- Count the number of recipes in all the lists
	local numRecipes = 0
	local header -- = nil
	if recipeListFromWorkshops ~= nil then
		numRecipes = numRecipes + #recipeListFromWorkshops
		header = TABLE_HEADER_PRODUCT
	end
	if buildingListFromCamps ~= nil then
		numRecipes = numRecipes + #buildingListFromCamps
		header = TABLE_HEADER_PRODUCT
	end
	if buildingListFromFarms ~= nil then
		numRecipes = numRecipes + #buildingListFromFarms
		header = TABLE_HEADER_PRODUCT
	end
	if recipeListFromServices ~= nil then
		if numRecipes > 0 then
			header = TABLE_HEADER_BOTH
		end
		numRecipes = numRecipes + #recipeListFromServices
		header = TABLE_HEADER_SERVICE
	end

	if numRecipes == 0 then
		return "No recipes found for product: " .. productName .. "."
	end

	RecipeView.startViewForProductAndBuilding(productName, buildingName, displayOverride, numRecipes, numIngredients, header)

	buildRowsForWorkshops(recipeListFromWorkshops, displayOverride, numIngredients, buildingName)

	buildRowsForServices(recipeListFromServices, displayOverride, numIngredients, buildingName)

	buildRowsForFarms(buildingListFromFarms, displayOverride, numIngredients, productID, buildingName)

	buildRowsForCamps(buildingListFromCamps, displayOverride, numIngredients, productID, buildingName)

	return RecipeView.endView(ingredientName, displayOverride)
end



function RecipeController.renderWithProduct(productName, displayOverride)

	-- Get lists from all buildings
	local productID = GoodsData.getGoodID(productName)
	if not productID then
		return "No product found with that name: " .. productName .. "."
	end
	local recipeListFromWorkshops = WorkshopsRecipesData.getAllRecipeIDsForProductID(productID)
	-- It could be the name of a service instead of the name of a product.
	local recipeListFromServices = ServicesRecipesData.getAllRecipeIDsWithServiceGoodID(productName)

	-- These result in a list of buildings, not recipes.
	local buildingListFromCamps = CampsData.getCampNamesWithRecipeProductID(productID)
	local buildingListFromFarms = FarmsData.getFarmNamesWithRecipeProductID(productID)

	-- Find out what the largest number of ingredients in this table is. Need
	-- this before we start building any of the beginning and the middle so
	-- that they are all the same.
	local numIngredients  = 1
	if recipeListFromWorkshops then
		for _, recipeID in ipairs(recipeListFromWorkshops) do
			local thisNum = WorkshopsRecipesData.getRecipeNumberOfIngredientsByID(recipeID)
			if numIngredients < thisNum	then
				numIngredients = thisNum
			end
		end
	end
	if recipeListFromServices then
		for _, recipeID in ipairs(recipeListFromServices) do
			local thisNum = WorkshopsRecipesData.getRecipeNumberOfServiceGoodsByID(recipeID)
			if numIngredients < thisNum	then
				numIngredients = thisNum
			end
		end
	end

	-- Count the number of recipes in all the lists
	local numRecipes = 0
	local header -- = nil
	if recipeListFromWorkshops ~= nil then
		numRecipes = numRecipes + #recipeListFromWorkshops
		header = TABLE_HEADER_PRODUCT
	end
	if buildingListFromCamps ~= nil then
		numRecipes = numRecipes + #buildingListFromCamps
		header = TABLE_HEADER_PRODUCT
	end
	if buildingListFromFarms ~= nil then
		numRecipes = numRecipes + #buildingListFromFarms
		header = TABLE_HEADER_PRODUCT
	end
	if recipeListFromServices ~= nil then
		if numRecipes > 0 then
			header = TABLE_HEADER_BOTH
		end
		numRecipes = numRecipes + #recipeListFromServices
		header = TABLE_HEADER_SERVICE
	end

	if numRecipes == 0 then
		return "No recipes found for product: " .. productName .. "."
	end

	RecipeView.startViewForProduct(productName, displayOverride, numRecipes, numIngredients, header)

	buildRowsForWorkshops(recipeListFromWorkshops, displayOverride, numIngredients)

	buildRowsForServices(recipeListFromServices, displayOverride, numIngredients)

	buildRowsForFarms(buildingListFromFarms, displayOverride, numIngredients, productID)

	buildRowsForCamps(buildingListFromCamps, displayOverride, numIngredients, productID)

	return RecipeView.endView(ingredientName, displayOverride)
end



function RecipeController.renderWithBuilding(buildingName, displayOverride)

	local recipeListFromWorkshops = WorkshopsData.getAllWorkshopRecipes(buildingName)
	local recipeListFromServices = InstitutionsData.getAllInstitutionRecipes(buildingName)

	local buildingListFromCamps = {}
	if CampsData.getCampNumberOfRecipes(buildingName) > 0 then
		buildingListFromCamps = { buildingName }
	end

	local buildingListFromFarms = {}
	if FarmsData.getFarmNumberOfRecipes(buildingName) > 0 then
		buildingListFromFarms = { buildingName }
	end

	-- Find out what the largest number of ingredients in this table is. Need
	-- this before we start building any of the beginning and the middle so
	-- that they are all the same.
	local numIngredients  = 1
	if recipeListFromWorkshops then
		for _, recipeID in ipairs(recipeListFromWorkshops) do
			local thisNum = WorkshopsRecipesData.getRecipeNumberOfIngredientsByID(recipeID)
			if numIngredients < thisNum	then
				numIngredients = thisNum
			end
		end
	end
	if recipeListFromServices then
		for _, recipeID in ipairs(recipeListFromServices) do
			local thisNum = WorkshopsRecipesData.getRecipeNumberOfServiceGoodsByID(recipeID)
			if numIngredients < thisNum	then
				numIngredients = thisNum
			end
		end
	end

	-- Count the number of recipes in all the lists
	local numRecipes = 0
	local header -- = nil
	if recipeListFromWorkshops ~= nil then
		numRecipes = numRecipes + #recipeListFromWorkshops
		header = TABLE_HEADER_PRODUCT
	end
	if buildingListFromCamps ~= nil then
		numRecipes = numRecipes + #buildingListFromCamps
		header = TABLE_HEADER_PRODUCT
	end
	if buildingListFromFarms ~= nil then
		numRecipes = numRecipes + #buildingListFromFarms
		header = TABLE_HEADER_PRODUCT
	end
	if recipeListFromServices ~= nil then
		if numRecipes > 0 then
			header = TABLE_HEADER_BOTH
		end
		numRecipes = numRecipes + #recipeListFromServices
		header = TABLE_HEADER_SERVICE
	end

	if numRecipes == 0 then
		return "No recipes found at building: " .. buildingName .. "."
	end

	RecipeView.startViewForBuilding(buildingName, displayOverride, numRecipes, numIngredients, header)

	buildRowsForWorkshops(recipeListFromWorkshops, displayOverride, numIngredients, buildingName)

	buildRowsForServices(recipeListFromServices, displayOverride, numIngredients, buildingName)

	buildRowsForFarms(buildingListFromFarms, displayOverride, numIngredients, nil, buildingName)

	buildRowsForCamps(buildingListFromCamps, displayOverride, numIngredients, nil, buildingName)

	return RecipeView.endView(ingredientName, displayOverride)
end

--endregion



--region Public methods

function RecipeController.renderRecipe(frame)

	-- Extract the template parameters.
	local productName = frame.args.product or frame.args[1]
	local buildingName = frame.args.building or frame.args[2]
	local ingredientName = frame.args.ingredientName
	local displayOverride = frame.args.display

	if ingredientName and ingredientNAme ~= "" then
		return RecipeController.renderWithIngredient(ingredientName, displayOverride)
	else
		if productName and productName ~= "" and buildingName and buildingName ~= "" then
			return RecipeController.renderWithProductAndBuilding(productName, buildingName, displayOverride)
		else
			if productName and productName ~= "" then
				return RecipeController.renderWithProduct(productName, displayOverride)
			else
				if buildingName and buildingName ~= "" then
					return RecipeController.renderWithBuilding(buildingName, displayOverride)
				else
					return "Unknown parameter in Recipe template."
				end
			end
		end
	end
end

--endregion

return RecipeController