Module:CollectorsData: Difference between revisions

From Against the Storm Official Wiki
(Created to manage data on collectors)
 
(Fixed pattern matching for grade)
 
(One intermediate revision by the same user not shown)
Line 154: Line 154:


local INDEX_RECIPE_GOOD_ID = "goodID"
local INDEX_RECIPE_GOOD_ID = "goodID"
local INDEX_RECIPE_STACK_SIZE = "stackSize"
local INDEX_RECIPE_GRADE = "grade"
local INDEX_RECIPE_GRADE = "grade"
local INDEX_RECIPE_PRODUCTION_TIME = "productionTime"
local INDEX_RECIPE_PRODUCTION_TIME = "productionTime"


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


--endregion
--endregion
Line 266: Line 267:
         local originalGradeIndex = headerLookup[LOOKUP_RECIPE_BASE_STRING .. i
         local originalGradeIndex = headerLookup[LOOKUP_RECIPE_BASE_STRING .. i
                 .. LOOKUP_GRADE_SUFFIX_STRING]
                 .. LOOKUP_GRADE_SUFFIX_STRING]
         local originalGatheringTimeIndex = headerLookup[LOOKUP_RECIPE_BASE_STRING .. i
         local originalTimeIndex = headerLookup[LOOKUP_RECIPE_BASE_STRING .. i
                 .. LOOKUP_TIME_SUFFIX_STRING]
                 .. LOOKUP_TIME_SUFFIX_STRING]


         local originalGoodID = originalBuilding[originalGoodIndex]
         local originalStackSize, originalGoodID = originalBuilding[originalGoodIndex]:match(PATTERN_SPLIT_STACK_AND_ID)


         -- Skip blank recipes.
         -- Skip blank recipes.
         if originalGoodID and originalGoodID ~= "" then
         if originalGoodID and originalGoodID ~= "" then
             newRecipe[INDEX_RECIPE_GOOD_ID] = originalGoodID
             newRecipe[INDEX_RECIPE_GOOD_ID] = originalGoodID
            newRecipe[INDEX_RECIPE_STACK_SIZE] = tonumber(originalStackSize)
             newRecipe[INDEX_RECIPE_GRADE] = tonumber(originalBuilding[originalGradeIndex]:match(PATTERN_CAPTURE_END_NUMBER))
             newRecipe[INDEX_RECIPE_GRADE] = tonumber(originalBuilding[originalGradeIndex]:match(PATTERN_CAPTURE_END_NUMBER))
             newRecipe[INDEX_RECIPE_PRODUCTION_TIME] = tonumber(originalBuilding[originalGatheringTimeIndex])
             newRecipe[INDEX_RECIPE_PRODUCTION_TIME] = tonumber(originalBuilding[originalTimeIndex])


             table.insert(recipes,newRecipe)
             table.insert(recipes,newRecipe)
Line 672: Line 674:


     return collector[INDEX_INITIALLY_ESSENTIAL]
     return collector[INDEX_INITIALLY_ESSENTIAL]
end
---
--- Retrieves the gathering area for the collector specified by its plain
--- language display name.
---
--- Returns nil if the collector named was not found.
---
---@param displayName string the plain language name of the collector
---@return number of tiles of gathering radius
function CollectorsData.getCollectorArea(displayName)
    local collector = CollectorsData.getAllDataForCollector(displayName)
    if not collector then
        return nil
    end
    return collector[INDEX_AREA]
end
end


Line 836: Line 817:
     end
     end


     return recipe[INDEX_RECIPE_GOOD_ID]
     return recipe[INDEX_RECIPE_GOOD_ID], recipe[INDEX_RECIPE_STACK_SIZE]
end
end



Latest revision as of 22:18, 5 December 2023

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

---
--- Module for compiling collectors 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:
---
--- area = CollectorsData.getCollectorArea(collectorName)
---
--- This will return the working radius of the collector. It is preferable to
--- call the getter methods with the name of the collector rather than
--- retrieving the entire data record for the collector. This way your code
--- stays protected from variations in this module. These getter methods are
--- called with the plain-language display name of the collector, as spelled
--- in the game, including punctuation.
---
--- * longDescription = CollectorsData.getCollectorDescription(collectorName)
--- * constructionCategory = CollectorsData.getCollectorCategory(collectorName)
--- * sizeX, sizeY = CollectorsData.getCollectorSize(collectorName)
--- * requiredGoodID, stackSize = CollectorsData.getCollectorRequiredGood(collectorName, requirementIndex)
--- * timeSeconds = CollectorsData.getCollectorConstructionTime(collectorName)
--- * cityScore = CollectorsData.getCollectorCityScore(collectorName)
--- * isMovable = CollectorsData.isCollectorMovable(collectorName)
--- * isInitiallyEssential = CollectorsData.isCollectorInitiallyEssential(collectorName)
--- * radius = CollectorsData.getCollectorArea(collectorName)
--- * storageCapacity = CollectorsData.getCollectorStorage(collectorName)
--- * workplaces = CollectorsData.getCollectorNumberOfWorkplaces(collectorName)
--- * goodID = CollectorsData.getCollectorRecipeProduct(collectorName, recipeIndex)
--- * gradeStars, gatheringSeconds = CollectorsData.getCollectorRecipeStats(collectorName, recipeIndex)
--- * iconFilename = CollectorsData.getCollectorIcon(collectorName)
---
--- And the following getter methods are all called with the collector's ID.
--- You will have an ID instead of a display name when dealing with other data
--- like recipes, species, etc.
---
--- * name = CollectorsData.getCollectorNameByID(id)
--- * iconFilename = CollectorsData.getCollectorIconByID(id)
---
--- You can retrieve all collectors that produce a certain good (with any grade of
--- efficiency) with the following getter:
---
--- collectorNamesTable = CollectorsData.getCollectorNamesWithRecipeProductID(productID)
---
--- There are methods to retrieve the lists of required goods, workplaces, and
--- recipes, but it is advised to instead use the getter methods
--- getCollectorRequiredGood, getCollectorNumberOfWorkplaces, and
--- getCollectorRecipe with an index desired, which is good for loops. If
--- you must get the tables, there are three "getAll" methods:
---
--- * requiredGoodsTable = CollectorsData.getAllCollectorRequiredGoods(collectorName)
--- * workplacesTable = CollectorsData.getAllCollectorWorkplaces(collectorName)
--- * recipesTable = CollectorsData.getAllCollectorRecipes(collectorName)
---
--- As a last resort, or if you need to transform the data structure, you can
--- call the method getAllDataForCollector(collectorName) or
--- getAllDataForCollectorByID(id). These return a whole record
--- from the data table corresponding to the required display name or ID.
---
--- The data table for collectors has the following structure:
---
--- collectorsTable = {
--- 	["collector1_ID"] = {
--- 		["id"] = "collector1_ID",
--- 		["displayName"] = "Plain Language Name",
--- 		["description"] = "A long string with some HTML entities too.",
--- 		["category"] = "Construction toolbar category",
--- 		["sizeX"] = 9, size in tiles
--- 		["sizeY"] = 9, size in tiles
--- 		["requiredGoods"] = {
--- 			[1] = { ["stackSize"] = 99, ["goodID"] = "good1_ID" },
--- 			[2] = { ["stackSize"] = 99, ["goodID"] = "good2_ID" },
--- 			[3] = { ... } or missing if fewer
--- 		},
--- 		["constructionTime"] = 99, number of seconds
--- 		["cityScore"] = 99, points?
--- 		["movable"] = true or false, if it can be moved at any cost
--- 		["initiallyEssential"] = true or false, if a new-game-starting blueprint
--- 		["area"] = 99, tile radius
--- 		["storage"] = 99, capacity
--- 		["workplaces"] = {
--- 			[1] = "Any",
--- 			[2] = "Any",
--- 			[3] = "Any",
--- 			[4] = ... representing the number of workplaces, or missing if fewer
--- 		},
--- 		["recipes"] = {
--- 			[1] = {
--- 				"goodID" = "good1_ID",
--- 				"grade" = 9, number of stars,
--- 				"gatheringTime" = 99, seconds,
--- 			}
--- 			[2] = { ... }
--- 			[3] = ... or missing if fewer
--- 		}
--- 	},
--- 	["collector2_ID"] = {
--- 		...
--- 	},
--- 	["collector3_ID"] = {
--- 		...
--- 	},
--- 	...
--- }
---
--- @module CollectorsData
local CollectorsData = {}



local CsvUtils = require("Module:CsvUtils")



--region Private member variables

--- Main data tables, like this: table[ID] = table containing data for that ID
local collectorsTable

--- Supporting table, list of names table[i] = name.
local collectorsNames

--- Lookup map. Built once and reused on subsequent calls within this session,
--- like this: table[display name] = id
local mapNamesToIDs
--- Lookup map. Built once and reused on subsequent calls within this session,
--- like this: table[goodID] = { name1, name2, ... }
local mapRecipeGoodIDsToCollectorNames

--endregion



--region Private constants

local DATA_TEMPLATE_NAME = "Template:Collectors_csv"

local INDEX_ID = "id"
local INDEX_NAME = "displayName"
local INDEX_DESCRIPTION = "description"
local INDEX_CATEGORY = "category"
local INDEX_SIZE_X = "sizeX"
local INDEX_SIZE_Y = "sizeY"
local INDEX_CONSTRUCTION_TIME = "constructionTime"
local INDEX_CITY_SCORE = "cityScore"
local INDEX_MOVABLE = "movable"
local INDEX_INITIALLY_ESSENTIAL = "initiallyEssential"
local INDEX_STORAGE = "storage"
local INDEX_REQUIRED_GOODS = "requiredGoods" -- table
local INDEX_WORKPLACES = "workplaces" -- table
local INDEX_RECIPES = "recipes" -- table

local INDEX_CONSTRUCTION_GOODS_STACK_SIZE = "stackSize"
local INDEX_CONSTRUCTION_GOODS_GOOD_ID = "goodID"

local INDEX_RECIPE_GOOD_ID = "goodID"
local INDEX_RECIPE_STACK_SIZE = "stackSize"
local INDEX_RECIPE_GRADE = "grade"
local INDEX_RECIPE_PRODUCTION_TIME = "productionTime"

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

--endregion



--region Private methods

---
--- Creates a new subtable containing the construction goods required for the
--- specified building.
---
--- @param originalBuilding table data record from which to make subtable
--- @param headerLookup table header lookup built from the CSV data
--- @return table subtable with the required construction goods
local function makeRequiredGoodsSubtable(originalBuilding, headerLookup)

    -- A constant we'll need only within this function.
    local REQ_GOOD_HEADER_BASE_STRING = "requiredGood"

    -- Copy the originals directly into a subtable.
    local requiredIndex1 = headerLookup[REQ_GOOD_HEADER_BASE_STRING .. "1"]
    local requiredIndex2 = headerLookup[REQ_GOOD_HEADER_BASE_STRING .. "2"]
    local requiredIndex3 = headerLookup[REQ_GOOD_HEADER_BASE_STRING .. "3"]

    local number1, id1 = originalBuilding[requiredIndex1]:match(PATTERN_SPLIT_STACK_AND_ID)
    local number2, id2 = originalBuilding[requiredIndex2]:match(PATTERN_SPLIT_STACK_AND_ID)
    local number3, id3 = originalBuilding[requiredIndex3]:match(PATTERN_SPLIT_STACK_AND_ID)

    -- don't add subtables that would just contain nils
    local requiredGoods = {
        number1 and id1 and { [INDEX_CONSTRUCTION_GOODS_STACK_SIZE] = tonumber(number1), [INDEX_CONSTRUCTION_GOODS_GOOD_ID] = id1 } or nil,
        number2 and id2 and { [INDEX_CONSTRUCTION_GOODS_STACK_SIZE] = tonumber(number2), [INDEX_CONSTRUCTION_GOODS_GOOD_ID] = id2 } or nil,
        number3 and id3 and { [INDEX_CONSTRUCTION_GOODS_STACK_SIZE] = tonumber(number3), [INDEX_CONSTRUCTION_GOODS_GOOD_ID] = id3 } or nil
    }

    return requiredGoods
end



---
--- Creates a new subtable containing the workplaces available in the specified
--- building.
---
--- @param originalBuilding table data record from which to make subtable
--- @param headerLookup table header lookup built from the CSV data
--- @return table subtable with the workplaces
local function makeWorkplacesSubtable(originalBuilding, headerLookup)

    -- A constant we'll need only within this function.
    local WORKPLACE_HEADER_BASE_STRING = "workplace"

    -- Copy the originals directly into a subtable.
    local workplaceIndex1 = headerLookup[WORKPLACE_HEADER_BASE_STRING .. "1"]
    local workplaceIndex2 = headerLookup[WORKPLACE_HEADER_BASE_STRING .. "2"]
    local workplaceIndex3 = headerLookup[WORKPLACE_HEADER_BASE_STRING .. "3"]
    local workplaceIndex4 = headerLookup[WORKPLACE_HEADER_BASE_STRING .. "4"]

    local workplace1 = originalBuilding[workplaceIndex1]
    local workplace2 = originalBuilding[workplaceIndex2]
    local workplace3 = originalBuilding[workplaceIndex3]
    local workplace4 = originalBuilding[workplaceIndex4]

    -- if it's not an empty string, then save that to the table, otherwise nil
    local workplaces = {
        (workplace1 ~= "" and workplace1) or nil,
        (workplace2 ~= "" and workplace2) or nil,
        (workplace3 ~= "" and workplace3) or nil,
        (workplace4 ~= "" and workplace4) or nil
    }

    return workplaces
end



---
--- Creates a new subtable containing the recipes available in the specified
--- building.
---
--- @param buildingName string the display name of the building
--- @param originalBuilding table data record from which to make subtable
--- @param headerLookup table header lookup built from the CSV data
--- @return table subtable with the recipe IDs
local function makeRecipesSubtable(buildingName, originalBuilding, headerLookup)

    -- A constant we'll need only within this function.
    local MAX_RECIPES = 3
    local LOOKUP_RECIPE_BASE_STRING = "recipe"
    local LOOKUP_GOOD_SUFFIX_STRING = "Good"
    local LOOKUP_GRADE_SUFFIX_STRING = "Grade"
    local LOOKUP_TIME_SUFFIX_STRING = "ProductionTime"

    if not mapRecipeGoodIDsToCollectorNames then
        mapRecipeGoodIDsToCollectorNames = {}
    end

    -- Copy the originals directly into a subtable.
    local recipes = {}
    for i = 1, MAX_RECIPES do

        -- A subtable for this recipe
        newRecipe = {}
        local originalGoodIndex = headerLookup[LOOKUP_RECIPE_BASE_STRING .. i
                .. LOOKUP_GOOD_SUFFIX_STRING]
        local originalGradeIndex = headerLookup[LOOKUP_RECIPE_BASE_STRING .. i
                .. LOOKUP_GRADE_SUFFIX_STRING]
        local originalTimeIndex = headerLookup[LOOKUP_RECIPE_BASE_STRING .. i
                .. LOOKUP_TIME_SUFFIX_STRING]

        local originalStackSize, originalGoodID = originalBuilding[originalGoodIndex]:match(PATTERN_SPLIT_STACK_AND_ID)

        -- Skip blank recipes.
        if originalGoodID and originalGoodID ~= "" then
            newRecipe[INDEX_RECIPE_GOOD_ID] = originalGoodID
            newRecipe[INDEX_RECIPE_STACK_SIZE] = tonumber(originalStackSize)
            newRecipe[INDEX_RECIPE_GRADE] = tonumber(originalBuilding[originalGradeIndex]:match(PATTERN_CAPTURE_END_NUMBER))
            newRecipe[INDEX_RECIPE_PRODUCTION_TIME] = tonumber(originalBuilding[originalTimeIndex])

            table.insert(recipes,newRecipe)

            if not mapRecipeGoodIDsToCollectorNames[originalGoodID] then
                mapRecipeGoodIDsToCollectorNames[originalGoodID] = {}
            end
            table.insert(mapRecipeGoodIDsToCollectorNames[originalGoodID], buildingName)
        end
    end

    return recipes
end



---
--- Transforms the originalCollectorsTable 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 originalCollectorsTable table of CSV-based data, with header row, data rows
--- @param headerLookup table lookup table of headers to get indexes
--- @return table better structured with IDs as keys
local function restructureCollectors(originalCollectorsTable, headerLookup)

    -- A few constants we need only in this function.
    local DATA_ROWS = 2
    local INDEX_ORIGINAL_ID = 1
    local INDEX_ORIGINAL_NAME = 2
    local INDEX_ORIGINAL_DESCRIPTION = 3
    local INDEX_ORIGINAL_CATEGORY = 4
    local INDEX_ORIGINAL_SIZE_X = 5
    local INDEX_ORIGINAL_SIZE_Y = 6
    -- Required goods are indexes 7, 8, 9
    local INDEX_ORIGINAL_CONSTRUCTION_TIME = 10
    local INDEX_ORIGINAL_CITY_SCORE = 11
    local INDEX_ORIGINAL_MOVABLE = 12
    local INDEX_ORIGINAL_ESSENTIAL = 13
    local INDEX_ORIGINAL_STORAGE = 14

    mapNamesToIDs = {}
    collectorsNames = {}

    local newCollectorsTable = {}
    for _, originalCollector in ipairs(originalCollectorsTable[DATA_ROWS]) do

        -- Copy over the content, mapping unhelpful indexes into headers keys.
        local newCollector = {}
        newCollector[INDEX_ID] = originalCollector[INDEX_ORIGINAL_ID]
        newCollector[INDEX_NAME] = originalCollector[INDEX_ORIGINAL_NAME]
        newCollector[INDEX_DESCRIPTION] = originalCollector[INDEX_ORIGINAL_DESCRIPTION]
        newCollector[INDEX_CATEGORY] = originalCollector[INDEX_ORIGINAL_CATEGORY]
        newCollector[INDEX_SIZE_X] = tonumber(originalCollector[INDEX_ORIGINAL_SIZE_X])
        newCollector[INDEX_SIZE_Y] = tonumber(originalCollector[INDEX_ORIGINAL_SIZE_Y])
        newCollector[INDEX_CITY_SCORE] = tonumber(originalCollector[INDEX_ORIGINAL_CITY_SCORE])
        newCollector[INDEX_MOVABLE] = originalCollector[INDEX_ORIGINAL_MOVABLE] == "True"
        newCollector[INDEX_INITIALLY_ESSENTIAL] = originalCollector[INDEX_ORIGINAL_ESSENTIAL] == "True"
        newCollector[INDEX_STORAGE] = tonumber(originalCollector[INDEX_ORIGINAL_STORAGE])
        newCollector[INDEX_CONSTRUCTION_TIME] = tonumber(originalCollector[INDEX_ORIGINAL_CONSTRUCTION_TIME])

        newCollector[INDEX_REQUIRED_GOODS] = makeRequiredGoodsSubtable(originalCollector, headerLookup)
        newCollector[INDEX_WORKPLACES] = makeWorkplacesSubtable(originalCollector, headerLookup)
        newCollector[INDEX_RECIPES] = makeRecipesSubtable(newCollector[INDEX_NAME], originalCollector, headerLookup)

        newCollectorsTable[ newCollector[INDEX_ID] ] = newCollector

        table.insert(collectorsNames, newCollector[INDEX_NAME])

        -- Also populate the map for looking up IDs with display names
        mapNamesToIDs[ newCollector[INDEX_NAME] ] = newCollector[INDEX_ID]
    end

    return newCollectorsTable
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 originalCollectorsTable, headerLookup = CsvUtils.extractTables(DATA_TEMPLATE_NAME)

    -- Now restructure to be more conducive.
    collectorsTable = restructureCollectors(originalCollectorsTable, headerLookup)
end



---
--- Uses the display name, which people are more familiar with, to find the
--- encoded ID of the building. Useful for retrieving the  data that is
--- indexed by ID.
---
--- Returns nil if the building with the specified name is not found.
---
--- @param displayName string plain-language name of the building to find
--- @return string ID of the building found, or nil if not found
local function findIDByName(displayName)

    -- At runtime, this should never be nil or empty, so throw an error.
    if not displayName or displayName == "" then
        error("Parameter is nil or empty for name: " .. displayName .. ".")
    end

    if not collectorsTable then
        loadData()
    end

    return mapNamesToIDs[displayName]
end

--endregion



--region Public methods

--- Retrieve the whole table of data for the specified collector. Instead of
--- this, you should probably be calling the individual getter methods.
---
--- Throws an error if called with nil or empty string. Returns nil if the
--- specified collector cannot be found.
---
--- @param id string ID of the collector
--- @return table containing the data with key-value pairs, or nil if not found
function CollectorsData.getAllDataForCollectorByID(id)

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

    if not collectorsTable then
        loadData()
    end

    return collectorsTable[id]
end



--- Retrieve the whole table of data for the specified collector. Instead of
--- this, you should probably be calling the individual getter methods.
---
--- Throws an error if called with nil or empty string. Returns nil if the
--- specified collector cannot be found.
---
--- @param displayName string plain language name of the collector
--- @return table containing the data with key-value pairs, or nil if not found
function CollectorsData.getAllDataForCollector(displayName)

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

    if not collectorsTable then
        loadData()
    end

    local id = findIDByName(displayName)
    if not id then
        return nil
    end

    return collectorsTable[id]
end



---
--- Retrieves the ID for the collector specified by its plain language
--- display name.
---
--- Returns nil if the collector named was not found.
---
---@param displayName string the plain language name of the collector
---@return string the ID of the specified collector
function CollectorsData.getCollectorID(displayName)

    local collector = CollectorsData.getAllDataForCollector(displayName)

    if not collector then
        return nil
    end

    return collector[INDEX_ID]
end



---
--- Retrieves the description for the collector specified by its plain
--- language display name.
---
--- Returns nil if the collector named was not found.
---
---@param displayName string the plain language name of the collector
---@return string the in-game description of the specified collector
function CollectorsData.getCollectorDescription(displayName)

    local collector = CollectorsData.getAllDataForCollector(displayName)

    if not collector then
        return nil
    end

    return collector[INDEX_DESCRIPTION]
end



---
--- Retrieves the construction toolbar category for the collector specified
--- by its plain language display name.
---
--- Returns nil if the collector named was not found.
---
---@param displayName string the plain language name of the collector
---@return string the category of the specified collector
function CollectorsData.getCollectorCategory(displayName)

    local collector = CollectorsData.getAllDataForCollector(displayName)

    if not collector then
        return nil
    end

    return collector[INDEX_CATEGORY]
end



---
--- Retrieves the 2x2 size for the collector specified by its plain language
--- display name.
---
--- Returns nil if the collector named was not found.
---
---@param displayName string the plain language name of the collector
---@return number the X-size of the collector
---@return number the Y-size of the collector
function CollectorsData.getCollectorSize(displayName)

    local collector = CollectorsData.getAllDataForCollector(displayName)

    if not collector then
        return nil
    end

    return collector[INDEX_SIZE_X], collector[INDEX_SIZE_Y]
end



---
--- Retrieves the goods required for construction for the collector specified
--- by its plain language display name, in a table that looks like this:
---
--- ["requiredGoods"] = {
---    [1] = { ["stackSize"] = 99, ["goodID"] = "good1_ID" },
---    [2] = { ["stackSize"] = 99, ["goodID"] = "good2_ID" },
---    [3] = { ... } or missing if fewer
--- }
---
--- Returns nil if the collector named was not found.
---
---@param displayName string the plain language name of the collector
---@return table of required goods
function CollectorsData.getAllCollectorRequiredGoods(displayName)

    local collector = CollectorsData.getAllDataForCollector(displayName)

    if not collector then
        return nil
    end

    return collector[INDEX_REQUIRED_GOODS]
end



---
--- Retrieves the specified required construction good for the collector
--- specified by its plain language display name.
---
--- Returns nil if the collector named was not found.
---
---@param displayName string the plain language name of the collector
---@param requirementIndex number which construction good to retrieve
---@return string the ID of the good that is required, or nil if none
---@return number the stack size of that good, or nil if none
function CollectorsData.getCollectorRequiredGood(displayName, requirementIndex)

    local requiredGoods = CollectorsData.getAllCollectorRequiredGoods(displayName)

    if not requiredGoods then
        return nil
    end

    local requirement = requiredGoods[requirementIndex]
    if not requirement then
        return nil
    end

    return requirement[INDEX_CONSTRUCTION_GOODS_GOOD_ID], requirement[INDEX_CONSTRUCTION_GOODS_STACK_SIZE]
end



---
--- Retrieves the construction time for the collector specified by its plain
--- language display name.
---
--- Returns nil if the collector named was not found.
---
---@param displayName string the plain language name of the collector
---@return number of seconds it takes to construct the collector
function CollectorsData.getCollectorConstructionTime(displayName)

    local collector = CollectorsData.getAllDataForCollector(displayName)

    if not collector then
        return nil
    end

    return collector[INDEX_CONSTRUCTION_TIME]
end



---
--- Retrieves the city score awarded for the collector specified by its plain
--- language display name.
---
--- Returns nil if the collector named was not found.
---
---@param displayName string the plain language name of the collector
---@return number of points for city score from having the collector
function CollectorsData.getCollectorCityScore(displayName)

    local collector = CollectorsData.getAllDataForCollector(displayName)

    if not collector then
        return nil
    end

    return collector[INDEX_CITY_SCORE]
end



---
--- Retrieves whether the collector specified by its plain language display
--- name can be moved, at any cost.
---
--- Returns nil if the collector named was not found.
---
---@param displayName string the plain language name of the collector
---@return boolean of whether the collector can be moved
function CollectorsData.isCollectorMovable(displayName)

    local collector = CollectorsData.getAllDataForCollector(displayName)

    if not collector then
        return nil
    end

    return collector[INDEX_MOVABLE]
end



---
--- Retrieves whether the collector specified by its plain language display
--- name has an essential starting blueprint for a new game profile.
---
--- Returns nil if the collector named was not found.
---
---@param displayName string the plain language name of the collector
---@return boolean of whether the collector's blueprint is essential
function CollectorsData.isCollectorInitiallyEssential(displayName)

    local collector = CollectorsData.getAllDataForCollector(displayName)

    if not collector then
        return nil
    end

    return collector[INDEX_INITIALLY_ESSENTIAL]
end



---
--- Retrieves the storage capacity of the collector specified by its plain
--- language display name.
---
--- Returns nil if the collector named was not found.
---
---@param displayName string the plain language name of the collector
---@return number of storage capacity at the collector
function CollectorsData.getCollectorStorage(displayName)

    local collector = CollectorsData.getAllDataForCollector(displayName)

    if not collector then
        return nil
    end

    return collector[INDEX_STORAGE]
end



---
--- Retrieves the table of workplaces for the collector specified by its
--- plain language display name.
---
--- Returns nil if the collector named was not found.
---
---@param displayName string the plain language name of the collector
---@return table of workplaces
function CollectorsData.getAllCollectorWorkplaces(displayName)

    local collector = CollectorsData.getAllDataForCollector(displayName)

    if not collector then
        return nil
    end

    return collector[INDEX_WORKPLACES]
end



---
--- Retrieves the number of workplaces for the collector specified by its
--- plain language display name.
---
--- Returns nil if the collector named was not found.
---
---@param displayName string the plain language name of the collector
---@return number of workplaces
function CollectorsData.getCollectorNumberOfWorkplaces(displayName)

    local collector = CollectorsData.getAllDataForCollector(displayName)

    if not collector then
        return nil
    end

    return #collector[INDEX_WORKPLACES]
end



---
--- Retrieves the table of recipes for the collector specified by its
--- plain language display name, in a table that looks like this:
---
--- ["recipes"] = {
--- 	[1] = "recipe1_ID",
--- 	[2] = "recipe2_ID",
--- 	[3] = "recipe3_ID",
--- 	[4] = ... or missing if fewer
--- }
---
--- Returns nil if the collector named was not found.
---
---@param displayName string the plain language name of the collector
---@return table of recipe IDs
function CollectorsData.getAllCollectorRecipes(displayName)

    local collector = CollectorsData.getAllDataForCollector(displayName)

    if not collector then
        return nil
    end

    return collector[INDEX_RECIPES]
end



---
--- Retrieves the number of recipes for the collector specified by its
--- plain language display name, in a table that looks like this:
---
--- Returns nil if the collector named was not found.
---
---@param displayName string the plain language name of the collector
---@return number of recipes at the collector
function CollectorsData.getCollectorNumberOfRecipes(displayName)

    local collector = CollectorsData.getAllDataForCollector(displayName)

    if not collector then
        return 0
    end

    local recipes = collector[INDEX_RECIPES]
    if not recipes then
        return 0
    end

    return #recipes
end



---
--- Retrieves the product's ID and the stack size produced by the specified
--- recipe at the collector specified by its plain language display name.
---
--- Returns nil if the collector named was not found.
---
---@param displayName string the plain language name of the collector
---@param recipeIndex number the index of the recipe ID
---@return string the ID of the good produced by the specified recipe
function CollectorsData.getCollectorRecipeProduct(displayName, recipeIndex)

    local recipes = CollectorsData.getAllCollectorRecipes(displayName)

    if not recipes then
        return nil
    end

    local recipe = recipes[recipeIndex]
    if not recipe then
        return nil
    end

    return recipe[INDEX_RECIPE_GOOD_ID], recipe[INDEX_RECIPE_STACK_SIZE]
end



---
--- Retrieves the other stats for specified recipe at the collector specified by
--- its plain language display name: the efficiency grade and the gathering
--- time.
---
--- Returns nil if the collector named was not found.
---
---@param displayName string the plain language name of the collector
---@param recipeIndex number the index of the recipe ID
---@return number of stars, the grade of the specified recipe
---@return number of seconds for gathering
function CollectorsData.getCollectorRecipeStats(displayName, recipeIndex)

    local recipes = CollectorsData.getAllCollectorRecipes(displayName)

    if not recipes then
        return nil
    end

    local recipe = recipes[recipeIndex]
    if not recipe then
        return nil
    end

    return recipe[INDEX_RECIPE_GRADE], recipe[INDEX_RECIPE_PRODUCTION_TIME]
end



---
--- Retrieves the icon filename for the collector specified by its plain
--- language display name.
---
--- Returns nil if the collector named was not found.
---
---@param displayName string the plain language name of the collector
---@return string the collector's icon filename, including the extension
function CollectorsData.getCollectorIcon(displayName)

    local collector = CollectorsData.getAllDataForCollector(displayName)

    if not collector then
        return nil
    end

    -- the base string of the icon is the ID. It has to be not nil to
    -- concatenate
    if collector[INDEX_ID] then
        return collector[INDEX_ID] .. "_icon.png"
    else
        return nil
    end
end



---
--- Retrieves the plain language display name for the collector specified by
--- its ID.
---
--- Returns nil if the collector named was not found.
---
---@param id string the ID of the collector
---@return string the collector's name as seen in-game
function CollectorsData.getCollectorNameByID(id)

    local collector = CollectorsData.getAllDataForCollectorByID(id)

    if not collector then
        return nil
    end

    return collector[INDEX_NAME]
end



---
--- Retrieves the icon filename for the the collector specified by its ID.
---
--- Returns nil if the collector named was not found.
---
---@param id string the ID of the collector
---@return string the collector's icon filename, including the extension
function CollectorsData.getCollectorIconByID(id)

    local collector = CollectorsData.getAllDataForCollectorByID(id)

    if not collector then
        return nil
    end

    -- the base string of the icon is the ID. It has to be not nil to
    -- concatenate
    if collector[INDEX_ID] then
        return collector[INDEX_ID] .. "_icon.png"
    else
        return nil
    end
end



---
--- Retrieve all display names of collectors that have a recipe that produces the
--- specified product ID.
---
---@param productID string the ID of the good to produce
---@return table of collector names that produce the specified good
function CollectorsData.getCollectorNamesWithRecipeProductID(productID)

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

    if not collectorsTable then
        loadData()
    end

    return mapRecipeGoodIDsToCollectorNames[productID]
end



---
--- Retrieves the name of the collector at the specified index.
---
---@param index number index
---@return string name of the building at the specified index
function CollectorsData.getCollectorNameFromDatabase(index)

    if not index then
        return nil
    end

    if not collectorsTable then
        loadData()
    end

    return collectorsNames[index]
end



---
--- Returns the number of buildings that can be retrieved from this
--- module's database.
---
---@return number of records
function CollectorsData.getNumberOfCollectorsInDatabase()

    if not collectorsTable then
        loadData()
    end

    return #collectorsNames
end

--endregion

return CollectorsData