Module:CampsData: Difference between revisions

From Against the Storm Official Wiki
(Added two new methods to support getting names from the database)
(Updated with common method names (like a building interface))
Line 107: Line 107:




--region Dependencies


local CsvUtils = require("Module:CsvUtils")
local CsvUtils = require("Module:CsvUtils")
-- Some dependencies are loaded lazily
local GoodsData
--endregion




Line 181: Line 188:
local function makeRequiredGoodsSubtable(originalCamp, campsHeaderLookup)
local function makeRequiredGoodsSubtable(originalCamp, campsHeaderLookup)


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


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


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


-- don't add subtables that would just contain nils
    -- don't add subtables that would just contain nils
local requiredGoods = {
    local requiredGoods = {
number1 and id1 and { [INDEX_CONSTRUCTION_GOODS_STACK_SIZE] = tonumber(number1), [INDEX_CONSTRUCTION_GOODS_GOOD_ID] = id1 } or nil,
        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,
        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
        number3 and id3 and { [INDEX_CONSTRUCTION_GOODS_STACK_SIZE] = tonumber(number3), [INDEX_CONSTRUCTION_GOODS_GOOD_ID] = id3 } or nil
}
    }


return requiredGoods
    return requiredGoods
end
end


Line 214: Line 221:
local function makeWorkplacesSubtable(originalCamp, campsHeaderLookup)
local function makeWorkplacesSubtable(originalCamp, campsHeaderLookup)


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


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


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


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


return workplaces
    return workplaces
end
end


Line 251: Line 258:
local function makeRecipesSubtable(campName, originalCamp, campsHeaderLookup)
local function makeRecipesSubtable(campName, originalCamp, campsHeaderLookup)


-- A constant we'll need only within this function.
    -- A constant we'll need only within this function.
local MAX_RECIPES = 3
    local MAX_RECIPES = 3
local LOOKUP_RECIPE_BASE_STRING = "recipe"
    local LOOKUP_RECIPE_BASE_STRING = "recipe"
local LOOKUP_GOOD_BASE_STRING = "SeekedDeposits"
    local LOOKUP_GOOD_BASE_STRING = "SeekedDeposits"
local LOOKUP_GRADE_BASE_STRING = "Grade"
    local LOOKUP_GRADE_BASE_STRING = "Grade"
local LOOKUP_GATHERING_TIME_BASE_STRING = "GatheringTime"
    local LOOKUP_GATHERING_TIME_BASE_STRING = "GatheringTime"


if not mapRecipeGoodIDsToCampNames then
    if not mapRecipeGoodIDsToCampNames then
mapRecipeGoodIDsToCampNames = {}
        mapRecipeGoodIDsToCampNames = {}
end
    end


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


-- A subtable for this recipe
        -- A subtable for this recipe
newRecipe = {}
        newRecipe = {}
local originalGoodIndex = campsHeaderLookup[LOOKUP_RECIPE_BASE_STRING .. i
        local originalGoodIndex = campsHeaderLookup[LOOKUP_RECIPE_BASE_STRING .. i
.. LOOKUP_GOOD_BASE_STRING]
                .. LOOKUP_GOOD_BASE_STRING]
local originalGradeIndex = campsHeaderLookup[LOOKUP_RECIPE_BASE_STRING .. i
        local originalGradeIndex = campsHeaderLookup[LOOKUP_RECIPE_BASE_STRING .. i
.. LOOKUP_GRADE_BASE_STRING]
                .. LOOKUP_GRADE_BASE_STRING]
local originalGatheringTimeIndex = campsHeaderLookup[LOOKUP_RECIPE_BASE_STRING .. i
        local originalGatheringTimeIndex = campsHeaderLookup[LOOKUP_RECIPE_BASE_STRING .. i
.. LOOKUP_GATHERING_TIME_BASE_STRING]
                .. LOOKUP_GATHERING_TIME_BASE_STRING]


local originalGoodID = originalCamp[originalGoodIndex]
        local originalGoodID = originalCamp[originalGoodIndex]


-- 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_GRADE] = tonumber(originalCamp[originalGradeIndex]:match(PATTERN_CAPTURE_END_NUMBER))
            newRecipe[INDEX_RECIPE_GRADE] = tonumber(originalCamp[originalGradeIndex]:match(PATTERN_CAPTURE_END_NUMBER))
newRecipe[INDEX_RECIPE_GATHERING_TIME] = tonumber(originalCamp[originalGatheringTimeIndex])
            newRecipe[INDEX_RECIPE_GATHERING_TIME] = tonumber(originalCamp[originalGatheringTimeIndex])


table.insert(recipes,newRecipe)
            table.insert(recipes,newRecipe)


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


return recipes
    return recipes
end
end


Line 308: Line 315:
local function restructureGathererHuts(originalCampsTable, campsHeaderLookup)
local function restructureGathererHuts(originalCampsTable, campsHeaderLookup)


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


mapNamesToIDs = {}
    mapNamesToIDs = {}
campsNames = {}
    campsNames = {}


local newCampsTable = {}
    local newCampsTable = {}
for _, originalCamp in ipairs(originalCampsTable[DATA_ROWS]) do
    for _, originalCamp in ipairs(originalCampsTable[DATA_ROWS]) do


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


newCamp[INDEX_REQUIRED_GOODS] = makeRequiredGoodsSubtable(originalCamp, campsHeaderLookup)
        newCamp[INDEX_REQUIRED_GOODS] = makeRequiredGoodsSubtable(originalCamp, campsHeaderLookup)
newCamp[INDEX_WORKPLACES] = makeWorkplacesSubtable(originalCamp, campsHeaderLookup)
        newCamp[INDEX_WORKPLACES] = makeWorkplacesSubtable(originalCamp, campsHeaderLookup)
newCamp[INDEX_RECIPES] = makeRecipesSubtable(newCamp[INDEX_NAME], originalCamp, campsHeaderLookup)
        newCamp[INDEX_RECIPES] = makeRecipesSubtable(newCamp[INDEX_NAME], originalCamp, campsHeaderLookup)


newCampsTable[ newCamp[INDEX_ID] ] = newCamp
        newCampsTable[ newCamp[INDEX_ID] ] = newCamp


table.insert(campsNames, newCamp[INDEX_NAME])
        table.insert(campsNames, newCamp[INDEX_NAME])


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


return newCampsTable
    return newCampsTable
end
end


Line 371: Line 378:
local function addWoodcuttersCampRow(originalCampsTable, campsHeaderLookup)
local function addWoodcuttersCampRow(originalCampsTable, campsHeaderLookup)


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


-- The woodcutters' camp is at index 1 in the data rows.
    -- The woodcutters' camp is at index 1 in the data rows.
local originalCamp = originalCampsTable[DATA_ROWS][1]
    local originalCamp = originalCampsTable[DATA_ROWS][1]


local newCamp = {}
    local newCamp = {}
newCamp[INDEX_ID] = originalCamp[INDEX_ORIGINAL_ID]
    newCamp[INDEX_ID] = originalCamp[INDEX_ORIGINAL_ID]
newCamp[INDEX_NAME] = originalCamp[INDEX_ORIGINAL_NAME]
    newCamp[INDEX_NAME] = originalCamp[INDEX_ORIGINAL_NAME]
newCamp[INDEX_DESCRIPTION] = originalCamp[INDEX_ORIGINAL_DESCRIPTION]
    newCamp[INDEX_DESCRIPTION] = originalCamp[INDEX_ORIGINAL_DESCRIPTION]
newCamp[INDEX_CATEGORY] = originalCamp[INDEX_ORIGINAL_CATEGORY]
    newCamp[INDEX_CATEGORY] = originalCamp[INDEX_ORIGINAL_CATEGORY]
newCamp[INDEX_SIZE_X] = tonumber(originalCamp[INDEX_ORIGINAL_SIZE_X])
    newCamp[INDEX_SIZE_X] = tonumber(originalCamp[INDEX_ORIGINAL_SIZE_X])
newCamp[INDEX_SIZE_Y] = tonumber(originalCamp[INDEX_ORIGINAL_SIZE_Y])
    newCamp[INDEX_SIZE_Y] = tonumber(originalCamp[INDEX_ORIGINAL_SIZE_Y])
newCamp[INDEX_CONSTRUCTION_TIME] = tonumber(originalCamp[INDEX_ORIGINAL_CONSTRUCTION_TIME])
    newCamp[INDEX_CONSTRUCTION_TIME] = tonumber(originalCamp[INDEX_ORIGINAL_CONSTRUCTION_TIME])
newCamp[INDEX_CITY_SCORE] = tonumber(originalCamp[INDEX_ORIGINAL_CITY_SCORE])
    newCamp[INDEX_CITY_SCORE] = tonumber(originalCamp[INDEX_ORIGINAL_CITY_SCORE])
newCamp[INDEX_MOVABLE] = originalCamp[INDEX_ORIGINAL_MOVABLE]
    newCamp[INDEX_MOVABLE] = originalCamp[INDEX_ORIGINAL_MOVABLE]
newCamp[INDEX_INITIALLY_ESSENTIAL] = originalCamp[INDEX_ORIGINAL_ESSENTIAL]
    newCamp[INDEX_INITIALLY_ESSENTIAL] = originalCamp[INDEX_ORIGINAL_ESSENTIAL]
newCamp[INDEX_AREA] = tonumber(originalCamp[INDEX_ORIGINAL_AREA])
    newCamp[INDEX_AREA] = tonumber(originalCamp[INDEX_ORIGINAL_AREA])
newCamp[INDEX_STORAGE] = tonumber(originalCamp[INDEX_ORIGINAL_STORAGE])
    newCamp[INDEX_STORAGE] = tonumber(originalCamp[INDEX_ORIGINAL_STORAGE])


newCamp[INDEX_REQUIRED_GOODS] = makeRequiredGoodsSubtable(originalCamp, campsHeaderLookup)
    newCamp[INDEX_REQUIRED_GOODS] = makeRequiredGoodsSubtable(originalCamp, campsHeaderLookup)
newCamp[INDEX_WORKPLACES] = makeWorkplacesSubtable(originalCamp, campsHeaderLookup)
    newCamp[INDEX_WORKPLACES] = makeWorkplacesSubtable(originalCamp, campsHeaderLookup)


-- Create a subtable of one recipe. This part of the record does not exist
    -- Create a subtable of one recipe. This part of the record does not exist
-- in the data and needs to be fabricated to match the other camps.
    -- in the data and needs to be fabricated to match the other camps.
local oneRecipe = {}
    local oneRecipe = {}
local recipe = {}
    local recipe = {}
recipe[INDEX_RECIPE_GOOD_ID] = WOODCUTTER_ID_WOOD
    recipe[INDEX_RECIPE_GOOD_ID] = WOODCUTTER_ID_WOOD
recipe[INDEX_RECIPE_GRADE] = WOODCUTTER_GRADE
    recipe[INDEX_RECIPE_GRADE] = WOODCUTTER_GRADE
recipe[INDEX_RECIPE_GATHERING_TIME] = WOODCUTTER_GATHERING_TIME
    recipe[INDEX_RECIPE_GATHERING_TIME] = WOODCUTTER_GATHERING_TIME
table.insert(oneRecipe, recipe)
    table.insert(oneRecipe, recipe)
newCamp[INDEX_RECIPES] = oneRecipe
    newCamp[INDEX_RECIPES] = oneRecipe


campsTable[newCamp[INDEX_ID]] = newCamp
    campsTable[newCamp[INDEX_ID]] = newCamp


mapNamesToIDs[newCamp[INDEX_NAME]] = newCamp[INDEX_ID]
    mapNamesToIDs[newCamp[INDEX_NAME]] = newCamp[INDEX_ID]


-- This is the only camp that harvests wood.
    -- This is the only camp that harvests wood.
mapRecipeGoodIDsToCampNames[WOODCUTTER_ID_WOOD] = {}
    mapRecipeGoodIDsToCampNames[WOODCUTTER_ID_WOOD] = {}
table.insert(mapRecipeGoodIDsToCampNames[WOODCUTTER_ID_WOOD], newCamp[INDEX_NAME])
    table.insert(mapRecipeGoodIDsToCampNames[WOODCUTTER_ID_WOOD], newCamp[INDEX_NAME])
end
end


Line 435: Line 442:
local function loadData()
local function loadData()


-- Utility module retrieves the data as basic, flat lua tables.
    -- Utility module retrieves the data as basic, flat lua tables.
local originalGathererHutsTable, gathererHutsHeaderLookup = CsvUtils.extractTables(GATHERER_HUTS_DATA_TEMPLATE_NAME)
    local originalGathererHutsTable, gathererHutsHeaderLookup = CsvUtils.extractTables(GATHERER_HUTS_DATA_TEMPLATE_NAME)


-- Now restructure to be more conducive.
    -- Now restructure to be more conducive.
campsTable = restructureGathererHuts(originalGathererHutsTable, gathererHutsHeaderLookup)
    campsTable = restructureGathererHuts(originalGathererHutsTable, gathererHutsHeaderLookup)


-- Now append the woodcutters' camp and fake recipe records for its
    -- Now append the woodcutters' camp and fake recipe records for its
local originalCampsTable, campsHeaderLookup = CsvUtils.extractTables(CAMPS_DATA_TEMPLATE_NAME)
    local originalCampsTable, campsHeaderLookup = CsvUtils.extractTables(CAMPS_DATA_TEMPLATE_NAME)


-- Now restructure and append a row for the camp, since there is only one record right now.
    -- Now restructure and append a row for the camp, since there is only one record right now.
addWoodcuttersCampRow(originalCampsTable, campsHeaderLookup)
    addWoodcuttersCampRow(originalCampsTable, campsHeaderLookup)
end
 
 
 
local function load()
    if not campsTable then
        loadData()
    end
end
end


Line 461: Line 476:
local function findCampIDByName(displayName)
local function findCampIDByName(displayName)


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


if not campsTable then
    if not campsTable then
loadData()
        loadData()
end
    end


return mapNamesToIDs[displayName]
    return mapNamesToIDs[displayName]
end
end


Line 489: Line 504:
function CampsData.getAllDataForCampByID(campID)
function CampsData.getAllDataForCampByID(campID)


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


if not campsTable then
    if not campsTable then
loadData()
        loadData()
end
    end


return campsTable[campID]
    return campsTable[campID]
end
end


Line 513: Line 528:
function CampsData.getAllDataForCamp(displayName)
function CampsData.getAllDataForCamp(displayName)


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


if not campsTable then
    if not campsTable then
loadData()
        loadData()
end
    end
 
local campID = findCampIDByName(displayName)
    local campID = findCampIDByName(displayName)
if not campID then
    if not campID then
return nil
        return nil
end
    end
 
return campsTable[campID]
    return campsTable[campID]
end
end


Line 542: Line 557:
function CampsData.getCampID(displayName)
function CampsData.getCampID(displayName)


local camp = CampsData.getAllDataForCamp(displayName)
    local camp = CampsData.getAllDataForCamp(displayName)


if not camp then
    if not camp then
return nil
        return nil
end
    end


return camp[INDEX_ID]
    return camp[INDEX_ID]
end
end


Line 563: Line 578:
function CampsData.getCampDescription(displayName)
function CampsData.getCampDescription(displayName)


local camp = CampsData.getAllDataForCamp(displayName)
    local camp = CampsData.getAllDataForCamp(displayName)


if not camp then
    if not camp then
return nil
        return nil
end
    end


return camp[INDEX_DESCRIPTION]
    return camp[INDEX_DESCRIPTION]
end
end


Line 584: Line 599:
function CampsData.getCampCategory(displayName)
function CampsData.getCampCategory(displayName)


local camp = CampsData.getAllDataForCamp(displayName)
    local camp = CampsData.getAllDataForCamp(displayName)


if not camp then
    if not camp then
return nil
        return nil
end
    end


return camp[INDEX_CATEGORY]
    return camp[INDEX_CATEGORY]
end
end


Line 606: Line 621:
function CampsData.getCampSize(displayName)
function CampsData.getCampSize(displayName)


local camp = CampsData.getAllDataForCamp(displayName)
    local camp = CampsData.getAllDataForCamp(displayName)


if not camp then
    if not camp then
return nil
        return nil
end
    end


return camp[INDEX_SIZE_X], camp[INDEX_SIZE_Y]
    return camp[INDEX_SIZE_X], camp[INDEX_SIZE_Y]
end
end


Line 633: Line 648:
function CampsData.getAllCampRequiredGoods(displayName)
function CampsData.getAllCampRequiredGoods(displayName)


local camp = CampsData.getAllDataForCamp(displayName)
    local camp = CampsData.getAllDataForCamp(displayName)


if not camp then
    if not camp then
return nil
        return nil
end
    end


return camp[INDEX_REQUIRED_GOODS]
    return camp[INDEX_REQUIRED_GOODS]
end
end


Line 656: Line 671:
function CampsData.getCampRequiredGood(displayName, requirementIndex)
function CampsData.getCampRequiredGood(displayName, requirementIndex)


local requiredGoods = CampsData.getAllCampRequiredGoods(displayName)
    local requiredGoods = CampsData.getAllCampRequiredGoods(displayName)


if not requiredGoods then
    if not requiredGoods then
return nil
        return nil
end
    end


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


return requirement[INDEX_CONSTRUCTION_GOODS_GOOD_ID], requirement[INDEX_CONSTRUCTION_GOODS_STACK_SIZE]
    return requirement[INDEX_CONSTRUCTION_GOODS_GOOD_ID], requirement[INDEX_CONSTRUCTION_GOODS_STACK_SIZE]
end
end


Line 682: Line 697:
function CampsData.getCampConstructionTime(displayName)
function CampsData.getCampConstructionTime(displayName)


local camp = CampsData.getAllDataForCamp(displayName)
    local camp = CampsData.getAllDataForCamp(displayName)


if not camp then
    if not camp then
return nil
        return nil
end
    end


return camp[INDEX_CONSTRUCTION_TIME]
    return camp[INDEX_CONSTRUCTION_TIME]
end
end


Line 703: Line 718:
function CampsData.getCampCityScore(displayName)
function CampsData.getCampCityScore(displayName)


local camp = CampsData.getAllDataForCamp(displayName)
    local camp = CampsData.getAllDataForCamp(displayName)


if not camp then
    if not camp then
return nil
        return nil
end
    end


return camp[INDEX_CITY_SCORE]
    return camp[INDEX_CITY_SCORE]
end
end


Line 724: Line 739:
function CampsData.isCampMovable(displayName)
function CampsData.isCampMovable(displayName)


local camp = CampsData.getAllDataForCamp(displayName)
    local camp = CampsData.getAllDataForCamp(displayName)


if not camp then
    if not camp then
return nil
        return nil
end
    end


return camp[INDEX_MOVABLE]
    return camp[INDEX_MOVABLE]
end
end


Line 745: Line 760:
function CampsData.isCampInitiallyEssential(displayName)
function CampsData.isCampInitiallyEssential(displayName)


local camp = CampsData.getAllDataForCamp(displayName)
    local camp = CampsData.getAllDataForCamp(displayName)


if not camp then
    if not camp then
return nil
        return nil
end
    end


return camp[INDEX_INITIALLY_ESSENTIAL]
    return camp[INDEX_INITIALLY_ESSENTIAL]
end
end


Line 766: Line 781:
function CampsData.getCampArea(displayName)
function CampsData.getCampArea(displayName)


local camp = CampsData.getAllDataForCamp(displayName)
    local camp = CampsData.getAllDataForCamp(displayName)


if not camp then
    if not camp then
return nil
        return nil
end
    end


return camp[INDEX_AREA]
    return camp[INDEX_AREA]
end
end


Line 787: Line 802:
function CampsData.getCampStorage(displayName)
function CampsData.getCampStorage(displayName)


local camp = CampsData.getAllDataForCamp(displayName)
    local camp = CampsData.getAllDataForCamp(displayName)


if not camp then
    if not camp then
return nil
        return nil
end
    end


return camp[INDEX_STORAGE]
    return camp[INDEX_STORAGE]
end
end


Line 808: Line 823:
function CampsData.getAllCampWorkplaces(displayName)
function CampsData.getAllCampWorkplaces(displayName)


local camp = CampsData.getAllDataForCamp(displayName)
    local camp = CampsData.getAllDataForCamp(displayName)


if not camp then
    if not camp then
return nil
        return nil
end
    end


return camp[INDEX_WORKPLACES]
    return camp[INDEX_WORKPLACES]
end
end


Line 829: Line 844:
function CampsData.getCampNumberOfWorkplaces(displayName)
function CampsData.getCampNumberOfWorkplaces(displayName)


local camp = CampsData.getAllDataForCamp(displayName)
    local camp = CampsData.getAllDataForCamp(displayName)


if not camp then
    if not camp then
return nil
        return nil
end
    end


return #camp[INDEX_WORKPLACES]
    return #camp[INDEX_WORKPLACES]
end
end


Line 857: Line 872:
function CampsData.getAllCampRecipes(displayName)
function CampsData.getAllCampRecipes(displayName)


local camp = CampsData.getAllDataForCamp(displayName)
    local camp = CampsData.getAllDataForCamp(displayName)


if not camp then
    if not camp then
return nil
        return nil
end
    end


return camp[INDEX_RECIPES]
    return camp[INDEX_RECIPES]
end
end


Line 878: Line 893:
function CampsData.getCampNumberOfRecipes(displayName)
function CampsData.getCampNumberOfRecipes(displayName)


local camp = CampsData.getAllDataForCamp(displayName)
    local camp = CampsData.getAllDataForCamp(displayName)


if not camp then
    if not camp then
return 0
        return 0
end
    end


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


return #recipes
    return #recipes
end
end


Line 905: Line 920:
function CampsData.getCampRecipeProduct(displayName, recipeIndex)
function CampsData.getCampRecipeProduct(displayName, recipeIndex)


local recipes = CampsData.getAllCampRecipes(displayName)
    local recipes = CampsData.getAllCampRecipes(displayName)


if not recipes then
    if not recipes then
return nil
        return nil
end
    end


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


return recipe[INDEX_RECIPE_GOOD_ID]
    return recipe[INDEX_RECIPE_GOOD_ID]
end
end


Line 934: Line 949:
function CampsData.getCampRecipeStats(displayName, recipeIndex)
function CampsData.getCampRecipeStats(displayName, recipeIndex)


local recipes = CampsData.getAllCampRecipes(displayName)
    local recipes = CampsData.getAllCampRecipes(displayName)


if not recipes then
    if not recipes then
return nil
        return nil
end
    end


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


return recipe[INDEX_RECIPE_GRADE], recipe[INDEX_RECIPE_GATHERING_TIME]
    return recipe[INDEX_RECIPE_GRADE], recipe[INDEX_RECIPE_GATHERING_TIME]
end
end


Line 960: Line 975:
function CampsData.getCampIcon(displayName)
function CampsData.getCampIcon(displayName)


local camp = CampsData.getAllDataForCamp(displayName)
    local camp = CampsData.getAllDataForCamp(displayName)


if not camp then
    if not camp then
return nil
        return nil
end
    end


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


Line 987: Line 1,002:
function CampsData.getCampNameByID(campID)
function CampsData.getCampNameByID(campID)


local camp = CampsData.getAllDataForCampByID(campID)
    local camp = CampsData.getAllDataForCampByID(campID)


if not camp then
    if not camp then
return nil
        return nil
end
    end


return camp[INDEX_NAME]
    return camp[INDEX_NAME]
end
end


Line 1,007: Line 1,022:
function CampsData.getCampIconByID(campID)
function CampsData.getCampIconByID(campID)


local camp = CampsData.getAllDataForCampByID(campID)
    local camp = CampsData.getAllDataForCampByID(campID)


if not camp then
    if not camp then
return nil
        return nil
end
    end


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


Line 1,032: Line 1,047:
function CampsData.getCampNamesWithRecipeProductID(productID)
function CampsData.getCampNamesWithRecipeProductID(productID)


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


if not campsTable then
    if not campsTable then
loadData()
        loadData()
end
    end


return mapRecipeGoodIDsToCampNames[productID]
    return mapRecipeGoodIDsToCampNames[productID]
end
end


Line 1,053: Line 1,068:
function CampsData.getCampNameFromDatabase(index)
function CampsData.getCampNameFromDatabase(index)


if not index then
    if not index then
return nil
        return nil
end
    end


if not campsTable then
    if not campsTable then
loadData()
        loadData()
end
    end


return campsNames[index]
    return campsNames[index]
end
end


Line 1,073: Line 1,088:
function CampsData.getNumberOfCampsInDatabase()
function CampsData.getNumberOfCampsInDatabase()


if not campsTable then
    if not campsTable then
loadData()
        loadData()
end
    end
 
    return #campsNames
end
 
--endregion
 
 
 
--region Public Building interface
-- getCategory
-- getCityScore
-- getConstructionCosts (as [goodName] = stack size)
-- getConstructionTime
-- getDescription
-- getIcon
-- getMovable
-- getName
-- getNumberOfWorkplaces
-- getSize (as "X x Y")
-- getStorage
 
function CampsData.getCategory(id)
    load()
    return campsTable[id] ~= nil and campsTable[id][INDEX_CATEGORY]
end
 
function CampsData.getCityScore(id)
    load()
    return campsTable[id] ~= nil and campsTable[id][INDEX_CITY_SCORE]
end
 
function CampsData.getConstructionCosts(id)
 
    load()
    GoodsData = require("Module:GoodsData")
 
    camp = campsTable[id]
    if not camp then
        return false
    end
 
    local constructionCosts = {}
    for _, stacks in ipairs(camp[INDEX_REQUIRED_GOODS]) do
        local goodName = GoodsData.getGoodNameByID(stacks[INDEX_CONSTRUCTION_GOODS_GOOD_ID])
        constructionCosts[goodName] = stacks[INDEX_CONSTRUCTION_GOODS_STACK_SIZE]
    end
 
    return constructionCosts
end
 
function CampsData.getConstructionTime(id)
    load()
    return campsTable[id] ~= nil and campsTable[id][INDEX_CONSTRUCTION_TIME]
end
 
function CampsData.getDescription(id)
    load()
    return campsTable[id] ~= nil and campsTable[id][INDEX_DESCRIPTION]
end
 
function CampsData.getIcon(id)
    load()
    return campsTable[id] ~= nil and id .. "_icon.png"
end
 
function CampsData.isMovable(id)
    load()
    return campsTable[id] ~= nil and campsTable[id][INDEX_MOVABLE]
end
 
function CampsData.getName(id)
    load()
    return campsTable[id] ~= nil and campsTable[id][INDEX_NAME]
end
 
function CampsData.getNumberOfWorkplaces(id)
    load()
    return campsTable[id] ~= nil and #campsTable[id][INDEX_WORKPLACES]
end
 
function CampsData.getSize(id)
    load()
 
    camp = campsTable[id]
    if not camp then
        return false
    end
 
    return camp[INDEX_SIZE_X] .. " × " .. camp[INDEX_SIZE_Y]
end


return #campsNames
function CampsData.getStorage(id)
    load()
    return campsTable[id] ~= nil and campsTable[id][INDEX_STORAGE]
end
end



Revision as of 01:50, 9 October 2024

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

---
--- Module for compiling camps 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 = CampsData.getCampArea(campName)
---
--- This will return the working radius of the camp. It is preferable to
--- call the getter methods with the name of the camp rather than
--- retrieving the entire data record for the camp. This way your code
--- stays protected from variations in this module. These getter methods are
--- called with the plain-language display name of the camp, as spelled
--- in the game, including punctuation.
---
--- * longDescription = CampsData.getCampDescription(campName)
--- * constructionCategory = CampsData.getCampCategory(campName)
--- * sizeX, sizeY = CampsData.getCampSize(campName)
--- * requiredGoodID, stackSize = CampsData.getCampRequiredGood(campName, requirementIndex)
--- * timeSeconds = CampsData.getCampConstructionTime(campName)
--- * cityScore = CampsData.getCampCityScore(campName)
--- * isMovable = CampsData.isCampMovable(campName)
--- * isInitiallyEssential = CampsData.isCampInitiallyEssential(campName)
--- * radius = CampsData.getCampArea(campName)
--- * storageCapacity = CampsData.getCampStorage(campName)
--- * workplaces = CampsData.getCampNumberOfWorkplaces(campName)
--- * goodID = CampsData.getCampRecipeProduct(campName, recipeIndex)
--- * gradeStars, gatheringSeconds = CampsData.getCampRecipeStats(campName, recipeIndex)
--- * iconFilename = CampsData.getCampIcon(campName)
---
--- And the following getter methods are all called with the camp's ID.
--- You will have an ID instead of a display name when dealing with other data
--- like recipes, species, etc.
---
--- * name = CampsData.getCampNameByID(campID)
--- * iconFilename = CampsData.getCampIconByID(campID)
---
--- You can retrieve all camps that produce a certain good (with any grade of
--- efficiency) with the following getter:
---
--- campNamesTable = CampsData.getCampNamesWithRecipeProductID(productID)
---
--- There are methods to retrieve the lists of required goods, workplaces, and
--- recipes, but it is advised to instead use the getter methods
--- getCampRequiredGood, getCampNumberOfWorkplaces, and
--- getCampRecipe with an index desired, which is good for loops. If
--- you must get the tables, there are three "getAll" methods:
---
--- * requiredGoodsTable = CampsData.getAllCampRequiredGoods(campName)
--- * workplacesTable = CampsData.getAllCampWorkplaces(campName)
--- * recipesTable = CampsData.getAllCampRecipes(campName)
---
--- As a last resort, or if you need to transform the data structure, you can
--- call the method getAllDataForCamp(campName) or
--- getAllDataForCampByID(campID). These return a whole record
--- from the data table corresponding to the required display name or ID.
---
--- The data table for camps has the following structure:
---
--- campsTable = {
--- 	["camp1_ID"] = {
--- 		["id"] = "camp1_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
--- 		}
--- 	},
--- 	["camp2_ID"] = {
--- 		...
--- 	},
--- 	["camp3_ID"] = {
--- 		...
--- 	},
--- 	...
--- }
---
--- @module CampsData
local CampsData = {}



--region Dependencies

local CsvUtils = require("Module:CsvUtils")

-- Some dependencies are loaded lazily
local GoodsData

--endregion



--region Private member variables

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

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

--- Lookup map. Built once and reused on subsequent calls within this session,
--- like this: table[display name] = campID
local mapNamesToIDs
--- Lookup map. Built once and reused on subsequent calls within this session,
--- like this: table[goodID] = { campName1, campName2, ... }
local mapRecipeGoodIDsToCampNames

--endregion



--region Private constants

local GATHERER_HUTS_DATA_TEMPLATE_NAME = "Template:GathererHuts_csv"
local CAMPS_DATA_TEMPLATE_NAME = "Template:Camps_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_AREA = "area"
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_GRADE = "grade"
local INDEX_RECIPE_GATHERING_TIME = "gatheringTime"

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

local WOODCUTTER_ID_WOOD = "[Mat Raw] Wood"
local WOODCUTTER_GRADE = 1
local WOODCUTTER_GATHERING_TIME = 2

--endregion



--region Private methods

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

    -- 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 = campsHeaderLookup[REQ_GOOD_HEADER_BASE_STRING .. "1"]
    local requiredIndex2 = campsHeaderLookup[REQ_GOOD_HEADER_BASE_STRING .. "2"]
    local requiredIndex3 = campsHeaderLookup[REQ_GOOD_HEADER_BASE_STRING .. "3"]

    local number1, id1 = originalCamp[requiredIndex1]:match(PATTERN_SPLIT_STACK_AND_ID)
    local number2, id2 = originalCamp[requiredIndex2]:match(PATTERN_SPLIT_STACK_AND_ID)
    local number3, id3 = originalCamp[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
--- camp.
---
--- @param originalCamp table camp data record from which to make subtable
--- @param campsHeaderLookup table header lookup built from the CSV data
--- @return table subtable with the workplaces
local function makeWorkplacesSubtable(originalCamp, campsHeaderLookup)

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

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

    local workplace1 = originalCamp[workplaceIndex1]
    local workplace2 = originalCamp[workplaceIndex2]
    local workplace3 = originalCamp[workplaceIndex3]
    local workplace4 = originalCamp[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
--- camp.
---
--- @param campName string the display name of the camp
--- @param originalCamp table camp data record from which to make subtable
--- @param campsHeaderLookup table header lookup built from the CSV data
--- @return table subtable with the recipe IDs
local function makeRecipesSubtable(campName, originalCamp, campsHeaderLookup)

    -- A constant we'll need only within this function.
    local MAX_RECIPES = 3
    local LOOKUP_RECIPE_BASE_STRING = "recipe"
    local LOOKUP_GOOD_BASE_STRING = "SeekedDeposits"
    local LOOKUP_GRADE_BASE_STRING = "Grade"
    local LOOKUP_GATHERING_TIME_BASE_STRING = "GatheringTime"

    if not mapRecipeGoodIDsToCampNames then
        mapRecipeGoodIDsToCampNames = {}
    end

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

        -- A subtable for this recipe
        newRecipe = {}
        local originalGoodIndex = campsHeaderLookup[LOOKUP_RECIPE_BASE_STRING .. i
                .. LOOKUP_GOOD_BASE_STRING]
        local originalGradeIndex = campsHeaderLookup[LOOKUP_RECIPE_BASE_STRING .. i
                .. LOOKUP_GRADE_BASE_STRING]
        local originalGatheringTimeIndex = campsHeaderLookup[LOOKUP_RECIPE_BASE_STRING .. i
                .. LOOKUP_GATHERING_TIME_BASE_STRING]

        local originalGoodID = originalCamp[originalGoodIndex]

        -- Skip blank recipes.
        if originalGoodID and originalGoodID ~= "" then
            newRecipe[INDEX_RECIPE_GOOD_ID] = originalGoodID
            newRecipe[INDEX_RECIPE_GRADE] = tonumber(originalCamp[originalGradeIndex]:match(PATTERN_CAPTURE_END_NUMBER))
            newRecipe[INDEX_RECIPE_GATHERING_TIME] = tonumber(originalCamp[originalGatheringTimeIndex])

            table.insert(recipes,newRecipe)

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

    return recipes
end



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

    -- 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_AREA = 14
    local INDEX_ORIGINAL_STORAGE = 15

    mapNamesToIDs = {}
    campsNames = {}

    local newCampsTable = {}
    for _, originalCamp in ipairs(originalCampsTable[DATA_ROWS]) do

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

        newCamp[INDEX_REQUIRED_GOODS] = makeRequiredGoodsSubtable(originalCamp, campsHeaderLookup)
        newCamp[INDEX_WORKPLACES] = makeWorkplacesSubtable(originalCamp, campsHeaderLookup)
        newCamp[INDEX_RECIPES] = makeRecipesSubtable(newCamp[INDEX_NAME], originalCamp, campsHeaderLookup)

        newCampsTable[ newCamp[INDEX_ID] ] = newCamp

        table.insert(campsNames, newCamp[INDEX_NAME])

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

    return newCampsTable
end



---
--- Creates a row for the only record in this table, the woodcutters' camp, in
--- a way that matches the other gathering camps or huts.
---
---@param originalCampsTable table the one-record table
---@param campsHeaderLookup table lookup table of headers to get indexes
---@return table one record to add to campsTable
local function addWoodcuttersCampRow(originalCampsTable, campsHeaderLookup)

    -- 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_AREA = 14
    local INDEX_ORIGINAL_STORAGE = 15

    -- The woodcutters' camp is at index 1 in the data rows.
    local originalCamp = originalCampsTable[DATA_ROWS][1]

    local newCamp = {}
    newCamp[INDEX_ID] = originalCamp[INDEX_ORIGINAL_ID]
    newCamp[INDEX_NAME] = originalCamp[INDEX_ORIGINAL_NAME]
    newCamp[INDEX_DESCRIPTION] = originalCamp[INDEX_ORIGINAL_DESCRIPTION]
    newCamp[INDEX_CATEGORY] = originalCamp[INDEX_ORIGINAL_CATEGORY]
    newCamp[INDEX_SIZE_X] = tonumber(originalCamp[INDEX_ORIGINAL_SIZE_X])
    newCamp[INDEX_SIZE_Y] = tonumber(originalCamp[INDEX_ORIGINAL_SIZE_Y])
    newCamp[INDEX_CONSTRUCTION_TIME] = tonumber(originalCamp[INDEX_ORIGINAL_CONSTRUCTION_TIME])
    newCamp[INDEX_CITY_SCORE] = tonumber(originalCamp[INDEX_ORIGINAL_CITY_SCORE])
    newCamp[INDEX_MOVABLE] = originalCamp[INDEX_ORIGINAL_MOVABLE]
    newCamp[INDEX_INITIALLY_ESSENTIAL] = originalCamp[INDEX_ORIGINAL_ESSENTIAL]
    newCamp[INDEX_AREA] = tonumber(originalCamp[INDEX_ORIGINAL_AREA])
    newCamp[INDEX_STORAGE] = tonumber(originalCamp[INDEX_ORIGINAL_STORAGE])

    newCamp[INDEX_REQUIRED_GOODS] = makeRequiredGoodsSubtable(originalCamp, campsHeaderLookup)
    newCamp[INDEX_WORKPLACES] = makeWorkplacesSubtable(originalCamp, campsHeaderLookup)

    -- Create a subtable of one recipe. This part of the record does not exist
    -- in the data and needs to be fabricated to match the other camps.
    local oneRecipe = {}
    local recipe = {}
    recipe[INDEX_RECIPE_GOOD_ID] = WOODCUTTER_ID_WOOD
    recipe[INDEX_RECIPE_GRADE] = WOODCUTTER_GRADE
    recipe[INDEX_RECIPE_GATHERING_TIME] = WOODCUTTER_GATHERING_TIME
    table.insert(oneRecipe, recipe)
    newCamp[INDEX_RECIPES] = oneRecipe

    campsTable[newCamp[INDEX_ID]] = newCamp

    mapNamesToIDs[newCamp[INDEX_NAME]] = newCamp[INDEX_ID]

    -- This is the only camp that harvests wood.
    mapRecipeGoodIDsToCampNames[WOODCUTTER_ID_WOOD] = {}
    table.insert(mapRecipeGoodIDsToCampNames[WOODCUTTER_ID_WOOD], newCamp[INDEX_NAME])
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 originalGathererHutsTable, gathererHutsHeaderLookup = CsvUtils.extractTables(GATHERER_HUTS_DATA_TEMPLATE_NAME)

    -- Now restructure to be more conducive.
    campsTable = restructureGathererHuts(originalGathererHutsTable, gathererHutsHeaderLookup)

    -- Now append the woodcutters' camp and fake recipe records for its
    local originalCampsTable, campsHeaderLookup = CsvUtils.extractTables(CAMPS_DATA_TEMPLATE_NAME)

    -- Now restructure and append a row for the camp, since there is only one record right now.
    addWoodcuttersCampRow(originalCampsTable, campsHeaderLookup)
end



local function load()
    if not campsTable then
        loadData()
    end
end



---
--- Uses the display name, which people are more familiar with, to find the
--- encoded ID of the camp. Useful for retrieving the  data that is
--- indexed by ID.
---
--- Returns nil if the camp with the specified name is not found.
---
--- @param displayName string plain-language name of the camp to find
--- @return string ID of the camp found, or nil if not found
local function findCampIDByName(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 the camp's name: " .. displayName .. ".")
    end

    if not campsTable then
        loadData()
    end

    return mapNamesToIDs[displayName]
end

--endregion



--region Public methods

--- Retrieve the whole table of data for the specified camp. 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 camp cannot be found.
---
--- @param campID string ID of the camp
--- @return table containing the data with key-value pairs, or nil if not found
function CampsData.getAllDataForCampByID(campID)

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

    if not campsTable then
        loadData()
    end

    return campsTable[campID]
end



--- Retrieve the whole table of data for the specified camp. 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 camp cannot be found.
---
--- @param displayName string plain language name of the camp
--- @return table containing the data with key-value pairs, or nil if not found
function CampsData.getAllDataForCamp(displayName)

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

    if not campsTable then
        loadData()
    end

    local campID = findCampIDByName(displayName)
    if not campID then
        return nil
    end

    return campsTable[campID]
end



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

    local camp = CampsData.getAllDataForCamp(displayName)

    if not camp then
        return nil
    end

    return camp[INDEX_ID]
end



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

    local camp = CampsData.getAllDataForCamp(displayName)

    if not camp then
        return nil
    end

    return camp[INDEX_DESCRIPTION]
end



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

    local camp = CampsData.getAllDataForCamp(displayName)

    if not camp then
        return nil
    end

    return camp[INDEX_CATEGORY]
end



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

    local camp = CampsData.getAllDataForCamp(displayName)

    if not camp then
        return nil
    end

    return camp[INDEX_SIZE_X], camp[INDEX_SIZE_Y]
end



---
--- Retrieves the goods required for construction for the camp 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 camp named was not found.
---
---@param displayName string the plain language name of the camp
---@return table of required goods
function CampsData.getAllCampRequiredGoods(displayName)

    local camp = CampsData.getAllDataForCamp(displayName)

    if not camp then
        return nil
    end

    return camp[INDEX_REQUIRED_GOODS]
end



---
--- Retrieves the specified required construction good for the camp
--- specified by its plain language display name.
---
--- Returns nil if the camp named was not found.
---
---@param displayName string the plain language name of the camp
---@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 CampsData.getCampRequiredGood(displayName, requirementIndex)

    local requiredGoods = CampsData.getAllCampRequiredGoods(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 camp specified by its plain
--- language display name.
---
--- Returns nil if the camp named was not found.
---
---@param displayName string the plain language name of the camp
---@return number of seconds it takes to construct the camp
function CampsData.getCampConstructionTime(displayName)

    local camp = CampsData.getAllDataForCamp(displayName)

    if not camp then
        return nil
    end

    return camp[INDEX_CONSTRUCTION_TIME]
end



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

    local camp = CampsData.getAllDataForCamp(displayName)

    if not camp then
        return nil
    end

    return camp[INDEX_CITY_SCORE]
end



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

    local camp = CampsData.getAllDataForCamp(displayName)

    if not camp then
        return nil
    end

    return camp[INDEX_MOVABLE]
end



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

    local camp = CampsData.getAllDataForCamp(displayName)

    if not camp then
        return nil
    end

    return camp[INDEX_INITIALLY_ESSENTIAL]
end



---
--- Retrieves the gathering area for the camp specified by its plain
--- language display name.
---
--- Returns nil if the camp named was not found.
---
---@param displayName string the plain language name of the camp
---@return number of tiles of gathering radius
function CampsData.getCampArea(displayName)

    local camp = CampsData.getAllDataForCamp(displayName)

    if not camp then
        return nil
    end

    return camp[INDEX_AREA]
end



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

    local camp = CampsData.getAllDataForCamp(displayName)

    if not camp then
        return nil
    end

    return camp[INDEX_STORAGE]
end



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

    local camp = CampsData.getAllDataForCamp(displayName)

    if not camp then
        return nil
    end

    return camp[INDEX_WORKPLACES]
end



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

    local camp = CampsData.getAllDataForCamp(displayName)

    if not camp then
        return nil
    end

    return #camp[INDEX_WORKPLACES]
end



---
--- Retrieves the table of recipes for the camp 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 camp named was not found.
---
---@param displayName string the plain language name of the camp
---@return table of recipe IDs
function CampsData.getAllCampRecipes(displayName)

    local camp = CampsData.getAllDataForCamp(displayName)

    if not camp then
        return nil
    end

    return camp[INDEX_RECIPES]
end



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

    local camp = CampsData.getAllDataForCamp(displayName)

    if not camp then
        return 0
    end

    local recipes = camp[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 camp specified by its plain language display name.
---
--- Returns nil if the camp named was not found.
---
---@param displayName string the plain language name of the camp
---@param recipeIndex number the index of the recipe ID
---@return string the ID of the good produced by the specified recipe
function CampsData.getCampRecipeProduct(displayName, recipeIndex)

    local recipes = CampsData.getAllCampRecipes(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]
end



---
--- Retrieves the other stats for specified recipe at the camp specified by
--- its plain language display name: the efficiency grade and the gathering
--- time.
---
--- Returns nil if the camp named was not found.
---
---@param displayName string the plain language name of the camp
---@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 CampsData.getCampRecipeStats(displayName, recipeIndex)

    local recipes = CampsData.getAllCampRecipes(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_GATHERING_TIME]
end



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

    local camp = CampsData.getAllDataForCamp(displayName)

    if not camp then
        return nil
    end

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



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

    local camp = CampsData.getAllDataForCampByID(campID)

    if not camp then
        return nil
    end

    return camp[INDEX_NAME]
end



---
--- Retrieves the icon filename for the the camp specified by its ID.
---
--- Returns nil if the camp named was not found.
---
---@param campID string the ID of the camp
---@return string the camp's icon filename, including the extension
function CampsData.getCampIconByID(campID)

    local camp = CampsData.getAllDataForCampByID(campID)

    if not camp then
        return nil
    end

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



---
--- Retrieve all display names of camps that have a recipe that produces the
--- specified product ID.
---
---@param productID string the ID of the good to produce
---@return table of camp names that produce the specified good
function CampsData.getCampNamesWithRecipeProductID(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 campsTable then
        loadData()
    end

    return mapRecipeGoodIDsToCampNames[productID]
end



---
--- Retrieves the name of the camp at the specified index.
---
---@param index number index
---@return string name of the building at the specified index
function CampsData.getCampNameFromDatabase(index)

    if not index then
        return nil
    end

    if not campsTable then
        loadData()
    end

    return campsNames[index]
end



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

    if not campsTable then
        loadData()
    end

    return #campsNames
end

--endregion



--region Public Building interface
-- getCategory
-- getCityScore
-- getConstructionCosts (as [goodName] = stack size)
-- getConstructionTime
-- getDescription
-- getIcon
-- getMovable
-- getName
-- getNumberOfWorkplaces
-- getSize (as "X x Y")
-- getStorage

function CampsData.getCategory(id)
    load()
    return campsTable[id] ~= nil and campsTable[id][INDEX_CATEGORY]
end

function CampsData.getCityScore(id)
    load()
    return campsTable[id] ~= nil and campsTable[id][INDEX_CITY_SCORE]
end

function CampsData.getConstructionCosts(id)

    load()
    GoodsData = require("Module:GoodsData")

    camp = campsTable[id]
    if not camp then
        return false
    end

    local constructionCosts = {}
    for _, stacks in ipairs(camp[INDEX_REQUIRED_GOODS]) do
        local goodName = GoodsData.getGoodNameByID(stacks[INDEX_CONSTRUCTION_GOODS_GOOD_ID])
        constructionCosts[goodName] = stacks[INDEX_CONSTRUCTION_GOODS_STACK_SIZE]
    end

    return constructionCosts
end

function CampsData.getConstructionTime(id)
    load()
    return campsTable[id] ~= nil and campsTable[id][INDEX_CONSTRUCTION_TIME]
end

function CampsData.getDescription(id)
    load()
    return campsTable[id] ~= nil and campsTable[id][INDEX_DESCRIPTION]
end

function CampsData.getIcon(id)
    load()
    return campsTable[id] ~= nil and id .. "_icon.png"
end

function CampsData.isMovable(id)
    load()
    return campsTable[id] ~= nil and campsTable[id][INDEX_MOVABLE]
end

function CampsData.getName(id)
    load()
    return campsTable[id] ~= nil and campsTable[id][INDEX_NAME]
end

function CampsData.getNumberOfWorkplaces(id)
    load()
    return campsTable[id] ~= nil and #campsTable[id][INDEX_WORKPLACES]
end

function CampsData.getSize(id)
    load()

    camp = campsTable[id]
    if not camp then
        return false
    end

    return camp[INDEX_SIZE_X] .. " × " .. camp[INDEX_SIZE_Y]
end

function CampsData.getStorage(id)
    load()
    return campsTable[id] ~= nil and campsTable[id][INDEX_STORAGE]
end

--endregion

return CampsData