Module:IngredientsHierarchy
From Against the Storm Official Wiki
Documentation for this module may be created at Module:IngredientsHierarchy/doc
---@module IngredientsHierarchy local IngredientsHierarchy = {} --region Dependencies local GoodsData = require("Module:GoodsData") local ServicesData = require("Module:ServicesData") local DataModelsWithRecipes = { require("Module:CampsData"), require("Module:FarmsData"), require("Module:FishingData"), require("Module:GatheringData"), require("Module:InstitutionsData"), require("Module:RainCollectorsData"), require("Module:WorkshopsData"), } local TEMPLATE_RESOURCE_LINK = "Resource_link" local TEMPLATE_SERVICE_LINK = "Service_link" --endregion --region Private constants local NODE_ICON_SIZE = "medium" local LEAF_ICON_SIZE = "small" local INDEX_OPTION_ID = "name" -- this is for backwards compatibility, it's actually an ID --endregion --region Private methods ---@private ---Generic table utility to merge two arrays into the first, ensuring uniqueness. --- ---@param mainArray table first array ---@param arrayToAdd table array to add to the first local function mergeArrays(mainArray, arrayToAdd) --Quickly return from edge cases if not arrayToAdd or #arrayToAdd < 1 then return mainArray end if not mainArray or #mainArray < 1 then return arrayToAdd end --Create a set for fast lookup of items already in mainArray local seen = {} for _, item in ipairs(mainArray) do seen[item] = true end --Append only unique items from arrayToAdd for _, item in ipairs(arrayToAdd) do if not seen[item] then table.insert(mainArray, item) seen[item] = true -- Mark as seen so we don't add it again end end return mainArray end ---@private ---Flattens the provided recipe data into just its ingredient IDs. --- ---@param DataModel BaseDataModel a data model that implements the recipe query interface ---@param recipeData table pair of recipe data retrieved from a data model ---@return table array of ingredient IDs local function getIngredientsList(DataModel, recipeData) if not recipeData then return {} end local ingredientsList = {} for i = 1, DataModel:getRecipeNumIngredientSlots(recipeData) do for j = 1, DataModel:getRecipeIngredientNumOptions(recipeData, i) do local ingredientID = DataModel:getRecipeIngredientOptionIDAt(recipeData, i, j) --Thankfully, ingredients are always goods, never services. table.insert(ingredientsList, ingredientID) end end return ingredientsList end function IngredientsHierarchy.makeHierarchy(rootID) --First go through all the data models and extract an array of ingredients, with no duplicates. local ingredientsList = {} for _, DataModel in ipairs(DataModelsWithRecipes) do local recipeList = DataModel:getIDsAndRecipesWhereProductID(rootID) --There will always be at least one recipe, but we don't know whether any have ingredients yet. if #recipeList > 0 then --Go through all of those recipes, still for just this data model, and pull out a flat list of ingredients. for _, recipe in ipairs(recipeList) do local newList = getIngredientsList(DataModel, recipe) --Check if it's a leaf node, if so, add to the running ingredients list if #newList > 0 then ingredientsList = mergeArrays(ingredientsList, newList) end end end end --Recurse only if it's not a leaf node, but we have to wait to do this until we get through all the data models. if #ingredientsList > 0 then --We need to remove any ingredients that turn into tables, but keep them in order and in an array with no gaps. To do this, we'll create a new list to return and re-add subtables and string alike, in order. local returnList = {} for _, newRoot in ipairs(ingredientsList) do local newNode = IngredientsHierarchy.makeHierarchy(newRoot) if type(newNode) == "table" then returnList[newRoot] = newNode else --When not a table, need to add the ingredient to the list we'll return table.insert(returnList, newRoot) end end return returnList else --No new ingredients, so just return the string itself return rootID end end ---@private ---Runs through the provided table, counting how many times each entry appears in the hierarchy. The second parameter is cumulative, while descending the tree. --- ---@param hierarchyTable table hierarchy of products and ingredients ---@param countingTable table (not needed on first call), the running tally function IngredientsHierarchy.countInstances(hierarchyTable, countingTable) --Only require the second parameter on recursion. if not countingTable then countingTable = {} end for key, item in pairs(hierarchyTable) do local id = "" --We recurse for tables, but we still need to add their items to the total if type(item) == "table" then IngredientsHierarchy.countInstances(item, countingTable) id = key else id = item end if not countingTable[id] then countingTable[id] = 1 else countingTable[id] = countingTable[id] + 1 end end return countingTable end ---@private ---Counts all the unique items in the provided table and sorts the counts. The result is a sorted array of pairs, where pair.id and pair.count store the ID of the good or service. --- ---@param hierarchyTable table hierarchy of products and ingredients ---@return table sorted array of pairs of IDs and counts local function countAndSort(hierarchyTable) local countingTable = countInstances(hierarchyTable) local sortedCountingTable = {} for ingredient, totalCount in pairs(countingTable) do table.insert(sortedCountingTable, { id = ingredient, count = totalCount }) end table.sort(sortedCountingTable, function(a, b) return a.count > b.count end) return sortedCountingTable end function IngredientsHierarchy.reduceBelowDepth(hierarchyTable, depth) local reducedTable = {} if depth == 0 then --This, conveniently, flattens the table, we just have to extract the names. countingTable = IngredientsHierarchy.countInstances(hierarchyTable) for id, _ in pairs(countingTable) do table.insert(reducedTable, id) end else --Recurse down each subtable for _, item in pairs(hierarchyTable) do if type(item) == "table" then table.insert(reducedTable, IngredientsHierarchy.reduceBelowDepth(item, depth - 1)) else table.insert(reducedTable, item) end end end return reducedTable end --endregion --region Public methods --function renderLink(displayName, level) -- -- local viewParameters = { -- ["name"] = displayName, -- ["size"] = level > 1 and LEAF_ICON_SIZE or NODE_ICON_SIZE, -- ["display"] = "notext", -- } -- -- local id = GoodsData.getGoodID(displayName) -- if id then -- --If the ID exists, then we use the resource link template. -- return "| " .. level .. " " .. mw.getCurrentFrame():expandTemplate{ -- title = TEMPLATE_RESOURCE_LINK, -- args = viewParameters, -- } -- else -- --If it's not a good, then use the service link template. -- return "| " .. level .. " " .. mw.getCurrentFrame():expandTemplate{ -- title = TEMPLATE_SERVICE_LINK, -- args = viewParameters, -- } -- end --end -- -- -- --function printHierarchy(hierarchy, level) -- -- for key, value in pairs(hierarchy) do -- if type(value) == "table" then -- mw.log(renderLink(key, level)) -- printHierarchy(value, level + 1) -- else -- --Lead nodes -- mw.log(renderLink(value, level)) -- end -- end --end function IngredientsHierarchy.main(frame) local productIDList = {} --Convert the parameters to IDs instead of names. for _, productName in ipairs(frame.args) do local productID = GoodsData.getGoodID(productName) if not productID then productID = ServicesData.getID(productName) end table.insert(productIDList, productID) end local totalHierarchy = {} for _, id in ipairs(productIDList) do totalHierarchy[id] = makeHierarchy(id) end local sortedCountingTable = countAndSort(totalHierarchy) return totalHierarchy, sortedCountingTable end return IngredientsHierarchy