Module:RecipeController2
From Against the Storm Official Wiki
Documentation for this module may be created at Module:RecipeController2/doc
--- @module RecipeController local RecipeController = {} --region Dependencies local DataModelsArray = { require("Module:CampsData2"), --require("Module:CollectorsData"), require("Module:FarmsData2"), require("Module:FishingData"), require("Module:GatheringData"), require("Module:InstitutionsData2"), require("Module:WorkshopsData2"), } local GoodsData = require("Module:GoodsData") local BaseDataModel = require("Module:BaseDataModel") local VIEW_TEMPLATE_START = "Recipe/view" local VIEW_TEMPLATE_ROW = "Recipe/view/row" local VIEW_TEMPLATE_END = "Recipe/view/end" --endregion --region Private constants local OPTION_ID = "name" -- this is for backwards compatibility, it's actually an ID local OPTION_AMOUNT = "amount" 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 member variables --endregion --region Private methods --- --- Get the ingredients from the specified recipe and return a list of --- the names and icons of the ingredients for that recipe. --- ---@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 --- --- Get the ingredients from the specified recipe and return a list of --- the names and icons of the ingredients for that recipe. --- ---@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 --- --- Main engine that builds the view based on the list of buildings from --- workshops, which are slightly different than other building lists. --- ---@param recipeListFromWorkshops table the list of recipes to render ---@param displayOverride string to control if the display isn't default ---@param numIngredients number of ingredient columns to display ---@param requiredBuilding string the name of the required building 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, requiredBuilding) end end end end --- --- Main engine that builds the view based on the list of buildings from --- services, which are slightly different than other building lists. --- ---@param recipeListFromServices table the list of recipes to render ---@param displayOverride string to control if the display isn't default ---@param numIngredients number of ingredient columns to display ---@param requiredBuilding string the name of the required building 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, requiredBuilding) end end end end --- --- Main engine that builds the view based on the list of buildings from --- farms, which are slightly different than other building lists. --- ---@param buildingListFromFarms table the list of recipes to render ---@param displayOverride string to control if the display isn't default ---@param numIngredients number of ingredient columns to display ---@param productID string of the product to render ---@param requiredBuilding string the name of the required building 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, requiredBuilding) end end end end end --- --- Main engine that builds the view based on the list of buildings from --- camps, which are slightly different than other building lists. --- ---@param buildingListFromCamps table the list of recipes to render ---@param displayOverride string to control if the display isn't default ---@param numIngredients number of ingredient columns to display ---@param productID string of the product to render ---@param requiredBuilding string the name of the required building 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, requiredBuilding) end end end end end --- --- Main engine that builds the view based on the list of recipes from --- collectors, , which are slightly different than other building lists. --- ---@param buildingListFromCollectors table the list of recipes to render ---@param displayOverride string to control if the display isn't default ---@param numIngredients number of ingredient columns to display ---@param productID string of the product to render ---@param requiredBuilding string the name of the required building local function buildRowsForCollectors(buildingListFromCollectors, displayOverride, numIngredients, productID, requiredBuilding) for _, buildingName in ipairs(buildingListFromCollectors 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, CollectorsData.getCollectorNumberOfRecipes(buildingName) do local goodID, stackSize = CollectorsData.getCollectorRecipeProduct(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 = CollectorsData.getCollectorIcon(buildingName) local gradeStars, productionTime = CollectorsData.getCollectorRecipeStats(buildingName, i) local ingredientsList = {} local productName = GoodsData.getGoodNameByID(goodID) local productIcon = GoodsData.getGoodIconByID(goodID) RecipeView.addRowForRecipe(displayOverride, numIngredients, buildingName, buildingIcon, gradeStars, productionTime, ingredientsList, stackSize, productName, productIcon, requiredBuilding) 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 if #intersection == 0 then return nil end return intersection end --endregion --region Public methods --- --- Compiles the data, organizes it appropriately and in a way that protects --- the view from needing the details, and sends it to the view for rendering --- before returning it to the template. --- ---@param ingredientName string the ingredient to base the table on ---@param buildingName string the building to base the table on ---@param displayOverride string to control the display if not default local function renderWithIngredientAndBuilding(ingredientName, buildingName, 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 recipeListFromWorkshopsRecipes = WorkshopsRecipesData.getAllRecipeIDsWithIngredientID(ingredientID) local recipeListFromWorkshops = WorkshopsData.getAllWorkshopRecipes(buildingName) recipeListFromWorkshops = findIntersection(recipeListFromWorkshopsRecipes, recipeListFromWorkshops) local recipeListFromServicesRecipes = ServicesRecipesData.getAllRecipeIDsWithServiceGoodID(ingredientID) local recipeListFromServices = InstitutionsData.getAllInstitutionRecipes(buildingName) recipeListFromServices = findIntersection(recipeListFromServicesRecipes, recipeListFromServices) -- 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 = ServicesRecipesData.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 else header = TABLE_HEADER_SERVICE end numRecipes = numRecipes + #recipeListFromServices end if numRecipes == 0 then return "No recipes found for ingredient " .. ingredientName .. " in the building " .. buildingName .. "." end RecipeView.startViewForIngredientAndBuilding(ingredientName, buildingName, displayOverride, numRecipes, numIngredients, header) buildRowsForWorkshops(recipeListFromWorkshops, displayOverride, numIngredients, buildingName) buildRowsForServices(recipeListFromServices, displayOverride, numIngredients, buildingName) return RecipeView.endView(displayOverride) end --- --- Compiles the data, organizes it appropriately and in a way that protects --- the view from needing the details, and sends it to the view for rendering --- before returning it to the template. --- ---@param ingredientName string the ingredient to base the table on ---@param displayOverride string to control the display if not default local function 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 = ServicesRecipesData.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 else header = TABLE_HEADER_SERVICE end numRecipes = numRecipes + #recipeListFromServices 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(displayOverride) end --- --- Compiles the data, organizes it appropriately and in a way that protects --- the view from needing the details, and sends it to the view for rendering --- before returning it to the template. --- ---@param productName string the product to base the table on ---@param buildingName string the building to base the table on ---@param displayOverride string to control the display if not default local function renderWithProductAndBuilding(productName, buildingName, displayOverride) -- Get lists from all buildings local productID = GoodsData.getGoodID(productName) local recipeListFromWorkshopsRecipes if productID then recipeListFromWorkshopsRecipes = WorkshopsRecipesData.getAllRecipeIDsForProductID(productID) end 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.getAllRecipeIDsForServiceName(productName) local recipeListFromServices = InstitutionsData.getAllInstitutionRecipes(buildingName) recipeListFromServices = findIntersection(recipeListFromServicesRecipes, recipeListFromServices) -- These result in a list of buildings, not recipes. local buildingListFromCamps local buildingListFromFarms local buildingListFromCollectors if productID then buildingListFromCamps = CampsData.getCampNamesWithRecipeProductID(productID) buildingListFromFarms = FarmsData.getFarmNamesWithRecipeProductID(productID) buildingListFromCollectors = CollectorsData.getCollectorNamesWithRecipeProductID(productID) end buildingListFromCamps = findIntersection(buildingListFromCamps, {buildingName} ) buildingListFromFarms = findIntersection(buildingListFromFarms, {buildingName}) buildingListFromCollectors = findIntersection(buildingListFromCollectors, {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 = ServicesRecipesData.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 buildingListFromCollectors ~= nil then numRecipes = numRecipes + #buildingListFromCollectors header = TABLE_HEADER_PRODUCT end if recipeListFromServices ~= nil then if numRecipes > 0 then header = TABLE_HEADER_BOTH else header = TABLE_HEADER_SERVICE end numRecipes = numRecipes + #recipeListFromServices end if numRecipes == 0 then return "No recipes found for product " .. productName .. " at the building " .. buildingName .. "." 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) buildRowsForCollectors(buildingListFromCollectors, displayOverride, numIngredients, productID, buildingName) return RecipeView.endView(displayOverride) end --- --- Compiles the data, organizes it appropriately and in a way that protects --- the view from needing the details, and sends it to the view for rendering --- before returning it to the template. --- ---@param productName string the product to base the table on ---@param displayOverride string to control the display if not default local function renderWithProduct(productName, displayOverride) -- Get lists from all buildings local productID = GoodsData.getGoodID(productName) local recipeListFromWorkshops if productID then recipeListFromWorkshops = WorkshopsRecipesData.getAllRecipeIDsForProductID(productID) end -- It could be the name of a service instead of the name of a product. local recipeListFromServices = ServicesRecipesData.getAllRecipeIDsForServiceName(productName) -- These result in a list of buildings, not recipes. local buildingListFromCamps local buildingListFromFarms local buildingListFromCollectors if productID then buildingListFromCamps = CampsData.getCampNamesWithRecipeProductID(productID) buildingListFromFarms = FarmsData.getFarmNamesWithRecipeProductID(productID) buildingListFromCollectors = CollectorsData.getCollectorNamesWithRecipeProductID(productID) 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 = ServicesRecipesData.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 buildingListFromCollectors ~= nil then numRecipes = numRecipes + #buildingListFromCollectors header = TABLE_HEADER_PRODUCT end if recipeListFromServices ~= nil then if numRecipes > 0 then header = TABLE_HEADER_BOTH else header = TABLE_HEADER_SERVICE end numRecipes = numRecipes + #recipeListFromServices 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) buildRowsForCollectors(buildingListFromCollectors, displayOverride, numIngredients, productID) return RecipeView.endView(displayOverride) end --- --- Compiles the data, organizes it appropriately and in a way that protects --- the view from needing the details, and sends it to the view for rendering --- before returning it to the template. --- ---@param buildingName string the building to base the table on ---@param displayOverride string to control the display if not default local function renderWithBuilding(buildingName, displayOverride) local recipeListFromWorkshops = WorkshopsData.getAllWorkshopRecipes(buildingName) local recipeListFromServices = InstitutionsData.getAllInstitutionRecipes(buildingName) -- For camps, farms, and collectors where recipes are stored in the -- building data file, just check to make sure the building has recipes -- from that data. If so, that's all we need to make the building list. local buildingListFromCamps = {} if CampsData.getCampNumberOfRecipes(buildingName) > 0 then buildingListFromCamps = { buildingName } end local buildingListFromFarms = {} if FarmsData.getFarmNumberOfRecipes(buildingName) > 0 then buildingListFromFarms = { buildingName } end local buildingListFromCollectors = {} if CollectorsData.getCollectorNumberOfRecipes(buildingName) > 0 then buildingListFromCollectors = { 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 = ServicesRecipesData.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 + CampsData.getCampNumberOfRecipes(buildingName) header = TABLE_HEADER_PRODUCT end if buildingListFromFarms ~= nil then numRecipes = numRecipes + FarmsData.getFarmNumberOfRecipes(buildingName) header = TABLE_HEADER_PRODUCT end if buildingListFromCollectors ~= nil then numRecipes = numRecipes + CollectorsData.getCollectorNumberOfRecipes(buildingName) header = TABLE_HEADER_PRODUCT end if recipeListFromServices ~= nil then if numRecipes > 0 then header = TABLE_HEADER_BOTH else header = TABLE_HEADER_SERVICE end numRecipes = numRecipes + #recipeListFromServices 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) buildRowsForCollectors(buildingListFromCollectors, displayOverride, numIngredients, nil, buildingName) return RecipeView.endView(displayOverride) end --endregion --region Private methods ---getFlatRecipeValues ---Extracts a handful of values from the provided recipe pair. --- ---@param recipeData table pair of recipe data retrieved from a data model ---@return string, number, number, string, number building ID, efficiency grade, production time, product ID, and product amount (respectively) local function getFlatRecipeValues(recipeData) local buildingID = BaseDataModel.getRecipeBuildingID(recipeData) local grade = BaseDataModel.getRecipeGrade(recipeData) local time = BaseDataModel.getRecipeTime(recipeData) local productID = BaseDataModel.getRecipeProductID(recipeData) local productAmount = BaseDataModel.getRecipeProductAmount(recipeData) return buildingID, grade, time, productID, productAmount end ---buildingIngredientsTable ---Extracts values from the provided recipe pair and builds an ingredients table for use in a Recipe object. --- ---@param recipeData table pair of recipe data retrieved from a data model ---@return table nested ingredients, options (ID and amount) function RecipeController.buildingIngredientsTable(recipeData) local ingredientsTable = {} for i = 1, BaseDataModel.getRecipeNumIngredientSlots(recipeData) do if not ingredientsTable[i] then ingredientsTable[i] = {} end for j = 1, BaseDataModel.getRecipeIngredientNumOptions(recipeData, i) do ingredientsTable[i][j] = { [OPTION_ID] = BaseDataModel.getRecipeIngredientOptionIDAt(recipeData, i, j), [OPTION_AMOUNT] = BaseDataModel.getRecipeIngredientOptionAmountAt(recipeData, i, j), } end end return ingredientsTable end ---resolveBuildingName ---Quickly runs through the data models and attempts with each to resolve the provided ID into a name. --- ---@param buildingID string the ID ---@return string the name, or nil if not found local function resolveBuildingName(buildingID) for _, dataModel in ipairs(DataModelsArray) do local buildingName = dataModel:getName(buildingID) if buildingName then return buildingName end end return nil end ---getRawRecipes ---Queries the data models with the supplied parameters to construct an array of Recipe objects storing the recipes found in the data model. --- ---Benchmarking: ~0.0003 seconds --- ---@param DataModel table a require'd data model that implements the recipe query interface, passed in for code reuse ---@param requiredProduct string the name of the product, or nil if any ---@param requiredBuilding string the name of the building, or nil if any ---@param requiredIngredient string the name of an ingredient, or nil if any ---@return table array of pairs of buildingID and recipe data function RecipeController.getRawRecipes(DataModel, requiredProduct, requiredBuilding, requiredIngredient) local rawRecipeList = {} if requiredProduct and requiredBuilding then rawRecipeList = DataModel:getIDsAndRecipesWhereProductIDAndBuildingID(requiredProduct, requiredBuilding) elseif requiredProduct then rawRecipeList = DataModel:getIDsAndRecipesWhereProductID(requiredProduct) elseif requiredIngredient and requiredBuilding then rawRecipeList = DataModel:getIDsAndRecipesWhereIngredientIDAndBuildingID(requiredIngredient, requiredBuilding) elseif requiredIngredient then rawRecipeList = DataModel:getIDsAndRecipesWhereIngredientID(requiredIngredient) elseif requiredBuilding then rawRecipeList = DataModel:getIDsAndRecipesWhereBuildingID(requiredBuilding) else error("You must specify a product, building, or ingredient. Please see the template documentation for how to use the parameters") end return rawRecipeList end ---compileRecipeLists ---Adds the second list to the first, but restructures into Recipe objects along the way. --- ---@param recipeObjectTable table 3-factor array of Recipe objects, by product, grade, amount ---@param rawRecipeTable table list of recipe pairs, as gotten from a data model ---@return table the same recipeObjectTable, but with new and updated entries local function compileRecipeLists(recipeObjectTable, rawRecipeTable) for _, pair in ipairs(rawRecipeTable) do local buildingID, grade, time, productID, productAmount = getFlatRecipeValues(pair) local ingredientsTable = RecipeController.buildingIngredientsTable(pair) local buildingName = resolveBuildingName(buildingID) -- Services identify their need by name, but goods to not. If it's a service, this is a simple renaming. local productName = productID if not BaseDataModel.isRecipeProvidingService(pair) then productName = GoodsData.getName(productID) end -- Now that we have everything extracted from rawRecipeTable for this pair, load it into recipeObjectTable, whether as a new Recipe object or adding a building to an existing Recipe object if one already exists. Recipes are uniquely identified by the 3-way combination of product, grade, and product amount. if not recipeObjectTable[productName] then recipeObjectTable[productName] = {} end if not recipeObjectTable[productName][grade] then recipeObjectTable[productName][grade] = {} end if not recipeObjectTable[productName][grade][productAmount] then -- Create a new Recipe object at this place in the table. recipeObjectTable[productName][grade][productAmount] = RecipeController.Recipe.new( { buildingName }, grade, time, productID, productAmount, ingredientsTable) else -- Add the building to the existing Recipe object at this place in the table. recipeObjectTable[productName][grade][productAmount]:addBuilding(buildingName) end end return recipeObjectTable end function RecipeController.getRecipesFromAllDataModels(requiredProduct, requiredBuilding, requiredIngredient) recipeObjectArray = {} for _, dataModel in ipairs(DataModelsArray) do local newRecipeList = RecipeController.getRawRecipes(dataModel, requiredProduct, requiredBuilding, requiredIngredient) recipeObjectArray = compileRecipeLists(recipeObjectArray, newRecipeList) end return recipeObjectArray end ---countMaxIngredients ---@param recipeList table ---@return table function RecipeController.countMaxIngredients(recipeList) end function RecipeController.calculateCaption(requiredProduct, requiredBuilding, requiredIngredient, recipeList) end --endregion --region Public classes local Recipe = {} -- This class available outside for read-only access RecipeController.Recipe = Recipe RecipeController.Recipe.OPTION_ID = OPTION_ID RecipeController.Recipe.OPTION_AMOUNT = OPTION_AMOUNT ---new ---constructs a new Recipe instance from the provided data. --- ---the ingredientsTable must follow this format: ---ingredientsTable = { --- --ingredient slots in recipe, between 1 and 3 --- [1] = { --- --options for that slot, between 1 and 6 --- [1] = { --- --each option's ID and amount --- [Recipe.OPTION_ID] = string, --- [Recipe.OPTION_STACK_SIZE] = number, --- }, --- [2] = ... --- }, --- [2] = ... ---} --- ---@param buildingArray table array of the names of buildings that make this recipe ---@param grade number of stars, between 0 and 3 ---@param time number of seconds to produce ---@param productName string name of the good produced ---@param productStackSize number of goods produced each cycle ---@param ingredientsTable table array of 1-3 ingredient slots, each with 1-6 options, each with name and amount (see doc above) function Recipe.new(buildingArray, grade, time, productName, productStackSize, ingredientsTable) local instance = {} setmetatable(instance, { __index = Recipe} ) -- allow this instance to use Recipe class methods if not buildingArray or type(buildingArray) ~= "table" or #buildingArray < 1 then error("Cannot construct new Recipe with an empty building list") end instance.buildingsArray = buildingArray if not grade or grade == "" then error("Cannot construct new Recipe with an empty grade.") elseif type(grade) ~= "number" or grade > 4 or grade < 0 then error ("Cannot construct new Recipe with an invalid grade value") end instance.grade = grade if not time or time == "" then error ("Cannot construct new Recipe with an empty production time") elseif type(time) ~= "number" or time < 0 then error ("Cannot construct new Recipe with an invalid production time value") end instance.time = time if not productName or productionName == "" then error ("Cannot construct new Recipe with an empty product name") end instance.productName = productName if not productStackSize or productStackSize == "" then error("Cannot construct new Recipe with an empty product amount") elseif type(productStackSize) ~= "number" or productStackSize < 1 then error("Cannot construct new Recipe with an invalid product amount value") end instance.productStackSize = productStackSize if not ingredientsTable or type(ingredientsTable) ~= "table" then error("Cannot construct new Recipe with an invalid ingredients table") end if #ingredientsTable > 3 then error("Cannot construct new Recipe with an ingredients table larger than 3 subtables") end for i, optionsArray in ipairs(ingredientsTable) do if not optionsArray or type(optionsArray) ~= "table" or #optionsArray < 1 then error("Cannot construct new Recipe with an empty options list (at index " .. i .. ")") end if #optionsArray > 6 then error("Cannot construct new Recipe with an options array larger than 6 subtables (at index " .. i .. ")") end for j, option in ipairs(optionsArray) do if not option or type(option) ~= "table" then error("Cannot construct new Recipe with an empty option (at index " .. i .. ", " .. j .. ")") end if not option[OPTION_ID] or option[OPTION_ID] == "" then error("Cannot construct a new Recipe with an empty option ID (at index " .. i .. ", " .. j .. ")") end if not option[OPTION_AMOUNT] or type(option[OPTION_AMOUNT]) ~= "number" then error("Cannot construct a new Recipe with an empty option amount (at index " .. i .. ", " .. j .. ")") end if option[OPTION_AMOUNT] < 1 then error("Cannot construct a new Recipe with an invalid option amount (at index" .. i .. ", " .. j .. ")") end end end instance.ingredientsTable = ingredientsTable return instance end ---addBuilding ---Adds the provided building to this Recipe object's list of buildings where the recipe is made. --- ---@param buildingName string name function Recipe:addBuilding(buildingName) if not self.buildingArray then self.buildingArray = { buildingName } else -- Skip duplicates. It shouldn't happen in 99% of cases, but just to be sure. for _, existingBuilding in ipairs(self.buildingArray) do if existingBuilding == buildingName then return end end table.insert(self.buildingArray, buildingName) end end --endregion --region Public methods function RecipeController.main(frame) local requiredProduct = frame.args.product local requiredBuilding = frame.args.building local requiredIngredient = frame.args.ingredient local displayOverride = frame.args.display -- recipeList is a 3-factor array of Recipe objects, by [product][grade][stackSize] local recipeList = {} for _, DataModel in ipairs(DataModelsArray) do recipeList = compileRecipeLists(recipeList, RecipeController.getRawRecipes(DataModel, requiredProduct, requiredBuilding, requiredIngredient)) end local maxIngredients = countMaxIngredients(recipeList) local caption = calculateCaption(requiredProduct, requiredBuilding, requiredIngredient, recipeList) local retStart = frame:expandTemplate{ title = VIEW_TEMPLATE_START, args = { ["maxingredients"] = maxIngredients, ["caption"] = caption, } } local retEnd frame:expandTemplate{ title = VIEW_TEMPLATE_END, args = {}, } return retStart .. retEnd end --endregion return RecipeController