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


