Module:WorkshopsData: Difference between revisions

From Against the Storm Official Wiki
(More typos sadge)
(Removed module from private methods; finished top luadoc)
Line 1: Line 1:
---
---
-- Module for compiling goods (or resources as some call them) information from  
--- Module for compiling workshops (or production buildings as they are called
-- wiki data sources.
--- in-game) information from wiki data sources. Restructures the flat data
--
--- tables that are produced from CsvUtils to make them more conducive to Lua
-- @module WorkshopsData
--- methods that need to display the information.
----
--- The standard way of using this module is a call like the following:
---
--- iconFilename = WorkshopsData.getWorkshopIcon(workshopName)
---
--- This will return a string corresponding to the filename of the icon of the
--- workshop. It is preferable to call getter methods with the name of the good
--- rather than retrieving the entire record for the good, so that your code
--- stays protected from variations in this module. The getter methods are all
--- called with the plain-language name of the good, as spelled in the game.
---
--- * longDescription = WorkshopsData.getWorkshopDescription(workshopName)
--- * constructionCategory = WorkshopsData.getWorkshopCategory(workshopName)
--- * sizeX, sizeY = WorkshopsData.getWorkshopSize(workshopName)
--- * requiredGoodsTable = WorkshopsData.getWorkshopRequiredGoods(workshopName)
--- * timeSeconds = WorkshopsData.getWorkshopConstructionTime(workshopName)
--- * cityScore = WorkshopsData.getWorkshopCityScore(workshopName)
--- * isMovable = WorkshopsData.isWorkshopMoveable(workshopName)
--- * isInitiallyEssential = WorkshopsData.isWorkshopInitiallyEssential(workshopName)
--- * storageCap = WorkshopsData.isWorkshopInitiallyEssential(workshopName)
--- * workplacesTable = WorkshopsData.getWorkshopWorkplaces(workshopName)
--- * recipesTable = WorkshopsData.getWorkshopRecipes(workshopName)
--- * iconFilename = WorkshopsData.getWorkshopIcon(workshopName)
---
--- As a last resort, or if you need to transform the data structure, you can
--- call the method getAllDataForWorkshop(displayName). This returns a whole
--- record from the data table corresponding to the requested display name.
---
--- The data table for workshops has the following structure:
---
--- workshopsTable = {
--- ["workshop1_ID"] = {
--- ["id"] = "workshop1_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
--- ["storage"] = 99, capacity
--- ["workplaces"] = {
--- [1] = "Any",
--- [2] = "Any",
--- [3] = "Any",
--- [4] = ... representing the number of workplaces, or missing if fewer
--- },
--- ["recipes"] = {
--- [1] = "recipe1_ID",
--- [2] = "recipe2_ID",
--- [3] = "recipe3_ID",
--- [4] = ... or missing if fewer
--- }
--- },
--- ["workshop2_ID"] = {
--- ...
--- },
--- ["workshop3_ID"] = {
--- ...
--- },
--- ...
--- }
---
--- @module WorkshopsData
local WorkshopsData = {}
local WorkshopsData = {}
local CsvUtils = require("Module:CsvUtils")
--region Private member variables
--- Main data table, like this: table[ID] = table containing data for that ID
local workshopsTable
--- Lookup map. Built once and reused on all subsequent calls within this
--- session, like this: table[displayName] = workshopID
local mapNamesToIDs
--endregion
--region Private constants
local DATA_TEMPLATE_NAME = "Template:Workshops_csv"
local HEADER_ROW = 1
local DATA_ROWS = 2
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_CITY_SCORE = "cityScore"
local INDEX_MOVABLE = "movable"
local INDEX_ESSENTIAL = "initiallyEssential"
local INDEX_STORAGE_CAP = "storage"
local INDEX_CONSTRUCTION_TIME = "constructionTime"
local INDEX_REQUIRED_GOODS = "requiredGoods"
local INDEX_WORKPLACES = "workplaces"
local INDEX_RECIPES = "recipes"
local INDEX_CONSTRUCTION_GOODS_STACK_SIZE = "stackSize"
local INDEX_CONSTRUCTION_GOODS_GOOD_ID = "goodID"
local PATTERN_SPLIT_STACK_AND_ID = "(%d+)%s([%[%]%s%a]+)"
--endregion
--region Private methods


---
---
-- Dependencies
--- Creates a new subtable containing the construction goods required for the
--- specified workshop.
---
---
local CsvUtils = require("Module:CsvUtils")
--- @param originalWorkshop table workshop data record from which to make subtable
--- @param workshopsHeaderLookup table header lookup built from the CSV data
--- @return table subtable with the required construction goods
function makeRequiredGoodsSubtable(originalWorkshop, workshopsHeaderLookup)
 
-- 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 = workshopsHeaderLookup[REQ_GOOD_HEADER_BASE_STRING .. "1"]
local requiredIndex2 = workshopsHeaderLookup[REQ_GOOD_HEADER_BASE_STRING .. "2"]
local requiredIndex3 = workshopsHeaderLookup[REQ_GOOD_HEADER_BASE_STRING .. "3"]
 
local number1, id1 = originalWorkshop[requiredIndex1]:match(PATTERN_SPLIT_STACK_AND_ID)
local number2, id2 = originalWorkshop[requiredIndex2]:match(PATTERN_SPLIT_STACK_AND_ID)
local number3, id3 = originalWorkshop[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] = number1, [INDEX_CONSTRUCTION_GOODS_GOOD_ID] = id1 } or nil,
number2 and id2 and { [INDEX_CONSTRUCTION_GOODS_STACK_SIZE] = number2, [INDEX_CONSTRUCTION_GOODS_GOOD_ID] = id2 } or nil,
number3 and id3 and { [INDEX_CONSTRUCTION_GOODS_STACK_SIZE] = number3, [INDEX_CONSTRUCTION_GOODS_GOOD_ID] = id3 } or nil
}
 
return requiredGoods
end
 
 
 
---
--- Creates a new subtable containing the workplaces available in the specified
--- workshop.
---
--- @param originalWorkshop table workshop data record from which to make subtable
--- @param workshopsHeaderLookup table header lookup built from the CSV data
--- @return table subtable with the workplaces
function makeWorkplacesSubtable(originalWorkshop, workshopsHeaderLookup)
 
-- A constant we'll need only within this function.
local WORKPLACE_HEADER_BASE_STRING = "workplace"
 
-- Copy the originals directly into a subtable.
local workplaceIndex1 = workshopsHeaderLookup[WORKPLACE_HEADER_BASE_STRING .. "1"]
local workplaceIndex2 = workshopsHeaderLookup[WORKPLACE_HEADER_BASE_STRING .. "2"]
local workplaceIndex3 = workshopsHeaderLookup[WORKPLACE_HEADER_BASE_STRING .. "3"]
local workplaceIndex4 = workshopsHeaderLookup[WORKPLACE_HEADER_BASE_STRING .. "4"]
 
local workplace1 = originalWorkshop[workplaceIndex1]
local workplace2 = originalWorkshop[workplaceIndex2]
local workplace3 = originalWorkshop[workplaceIndex3]
local workplace4 = originalWorkshop[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
 
 


---
---
-- Constants for this module
--- Creates a new subtable containing the recipes available in the specified
--- workshop.
---
---
local WORKSHOPS_DATA_TEMPLATE_NAME = "Template:Workshops_csv"
--- @param originalWorkshop table workshop data record from which to make subtable
--- @param workshopsHeaderLookup table header lookup built from the CSV data
--- @return table subtable with the recipe IDs
function makeRecipesSubtable(originalWorkshop, workshopsHeaderLookup)
 
-- A constant we'll need only within this function.
local LOOKUP_RECIPE_BASE_STRING = "recipe"
 
-- Copy the originals directly into a subtable.
local recipeIndex1 = workshopsHeaderLookup[LOOKUP_RECIPE_BASE_STRING .. "1"]
local recipeIndex2 = workshopsHeaderLookup[LOOKUP_RECIPE_BASE_STRING .. "2"]
local recipeIndex3 = workshopsHeaderLookup[LOOKUP_RECIPE_BASE_STRING .. "3"]
local recipeIndex4 = workshopsHeaderLookup[LOOKUP_RECIPE_BASE_STRING .. "4"]
 
local recipe1 = originalWorkshop[recipeIndex1]
local recipe2 = originalWorkshop[recipeIndex2]
local recipe3 = originalWorkshop[recipeIndex3]
local recipe4 = originalWorkshop[recipeIndex4]


local INDEX_WORKSHOP_ID = 1
local recipes = {
local INDEX_WORKSHOP_NAME = 2
(recipe1 ~= "" and recipe1) or nil,
local INDEX_WORKSHOP_DESCRIPTION = 3
(recipe2 ~= "" and recipe2) or nil,
local INDEX_WORKSHOP_CATEGORY = 4
(recipe3 ~= "" and recipe3) or nil,
local INDEX_WORKSHOP_SIZE_X = 5
(recipe4 ~= "" and recipe4) or nil
local INDEX_WORKSHOP_SIZE_Y = 6
}
local INDEX_WORKSHOP_CITY_SCORE = 7
local INDEX_WORKSHOP_MOVABLE = 8
local INDEX_WORKSHOP_INITIALLY_ESSENTIAL = 9
local INDEX_WORKSHOP_STORAGE = 10
local INDEX_WORKSHOP_CONSTRUCTION_TIME = 11
local INDEX_WORKSHOP_REQUIRED_GOODS = 12
local INDEX_WORKSHOP_WORKPLACES = 13
local INDEX_WORKSHOP_RECIPES = 14


local HEADER_ROW = 1
return recipes
local DATA_ROWS = 2
end


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




---
---
-- Private member variables
--- Transforms the originalWorkshopTable 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 originalWorkshopsTable table of CSV-based data, with header row, data rows
--- @param workshopsHeaderLookup table lookup table of headers to get indexes
--- @return table better structured with IDs as keys
function restructureWorkshopsTable(originalWorkshopsTable, workshopsHeaderLookup)
-- A few constants we'll need only within this function.
local HEADER_REQUIRED_GOODS_BASE = "requiredGood"
local HEADER_WORKPLACE_BASE = "workplace"
local HEADER_RECIPES_BASE = "recipe"


-- Main data tables. Populated from CSV data and organized better for Lua.
mapNamesToIDs = {}


-- like this: table[workshopID] = table containing workshops data
local newWorkshopTable = {}
local workshopsTable
for _, originalWorkshop in ipairs (originalWorkshopsTable[DATA_ROWS]) do
 
-- Copy over the content, mapping unhelpful indexes into headers keys.
local newWorkshop = {}
for index, key in pairs(originalWorkshopsTable[HEADER_ROW]) do
 
-- Convert string versions of true/false to actual booleans
if key == INDEX_MOVABLE or key == INDEX_ESSENTIAL then
newWorkshop[key] = originalWorkshop[index] == "True"
else
-- Convert a some of the strings to subtables. But only once
-- per group of related strings.
if key:sub(1, #HEADER_REQUIRED_GOODS_BASE) == HEADER_REQUIRED_GOODS_BASE then
if not newWorkshop[INDEX_REQUIRED_GOODS] then
newWorkshop[INDEX_REQUIRED_GOODS] = makeRequiredGoodsSubtable(originalWorkshop, workshopsHeaderLookup)
end
else
if key:sub(1, #HEADER_WORKPLACE_BASE) == HEADER_WORKPLACE_BASE then
if not newWorkshop[INDEX_WORKPLACES] then
newWorkshop[INDEX_WORKPLACES] = makeWorkplacesSubtable(originalWorkshop, workshopsHeaderLookup)
end
else
if key:sub(1, #HEADER_RECIPES_BASE) == HEADER_RECIPES_BASE then
if not newWorkshop[INDEX_RECIPES] then
newWorkshop[INDEX_RECIPES] = makeRecipesSubtable(originalWorkshop, workshopsHeaderLookup)
end
else
-- Everything else can be directly copied over.
newWorkshop[key] = originalWorkshop[index]
end
end
end
end
end
 
newWorkshopTable[ newWorkshop[INDEX_ID] ] = newWorkshop


-- Lookup map. Built once and reused on subsequent calls
-- Also populate the map for looking up IDs with display names
-- like this:  table[display name] = workshopID
mapNamesToIDs[newWorkshop[INDEX_NAME]] = newWorkshop[INDEX_ID]
local workshopNameToWorkshopID
end
 
return newWorkshopTable
end






---
---
-- Data loader function. Calls the data templates, restructures the data to be
--- Data loader function that uses the utility module and restructures the data
-- useful for getter methods, and makes a merge table.
--- 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
-- This method is called the first time any of the actual template methods are
--- data table has not yet been populated in the current session.
-- invoked and they see that the data tables are nil. This method populates
-- them and reorganizes them for easier use (and easier code).
function loadData()
function loadData()
 
-- Use the CSV utility module to load the data templates we need for
-- Utility module retrieves the data as basic, flat lua tables.
-- resources.
local originalWorkshopsTable, workshopsHeaderLookup = CsvUtils.luaTableFromCSV(CsvUtils.extractCSV(DATA_TEMPLATE_NAME))
local originalWorkshopsTable, workshopsHeaderLookup = CsvUtils.luaTableFromCSV(CsvUtils.extractCSV(WORKSHOPS_DATA_TEMPLATE_NAME))
 
-- Now restructure to be more conducive for data about goods.
-- Now restructure the table so subtables can be passed to member functions
workshopsTable = restructureWorkshopsTable(originalWorkshopsTable, workshopsHeaderLookup)
-- for cleaner code.
workshopsTable = restructureWorkshopTable(originalWorkshopsTable, workshopsHeaderLookup)
end
end


Line 73: Line 315:


---
---
-- Retrieve all data for the specified workshop. Returned items look like this:
--- Uses the display name, which people are more familiar with, to find the
-- workshop {
--- encoded ID of the workshop. Useful for retrieving the workshop data that is
-- id
--- indexed by ID.
-- display name
---
-- description
--- Returns nil if the workshop with the specified name is not found.
-- category
---
-- size x
--- @param displayName string plain-language name of the workshop to find
-- size y
--- @return string ID of the workshop found, or nil if not found
-- city score
function findWorkshopIDByName(displayName)
-- movable
 
-- initially essential
-- At runtime, this should never be nil or empty, so throw an error.
-- storage
if not displayName or displayName == "" then
-- construction time
error("Parameter is nil or empty for the workshop's name: " .. displayName .. ".")
-- required goods (just their IDs) {
end
-- #1 { stack size, id }
 
-- #2 { stack size, id }
-- #3 { stack size, id }
-- }
-- workplaces {
-- workplace #1
-- workplace #2
-- workplace #3
-- workplace #4
-- }
-- recipes (just their IDs) {
-- recipe #1
-- recipe #2
-- recipe #3
-- recipe #4
-- }
-- }
--
-- @param workshopName plain language name of the workshop
-- @return a table containing the data for the specified workshop
function WorkshopsData.getAllDataForWorkshop(workshopName)
if not workshopsTable then
if not workshopsTable then
loadData()
loadData()
end
end
 
local targetWorkshopID = findWorkshopIDByName(workshopName)
return mapNamesToIDs[displayName]
if not targetWorkshopID then
end
error("No building found. Please check spelling and any punctuation like an apostrophe: " .. workshopName)
 
--endregion
 
 
 
--region Public methods
 
---
--- Retrieve the whole table of data for the specified workshop. 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 workshop cannot be found.
---
--- @param displayName string plain language name of the workshop
--- @return table containing the data for the specified workshop with key-value pairs, or nil if not found
function WorkshopsData.getAllDataForWorkshop(displayName)
 
-- At runtime, this should never be nil or empty.
if not displayName or displayName == "" then
error("Parameter is nil or empty for the workshop's name: " .. displayName .. ".")
end
 
if not workshopsTable then
WorkshopsData.loadData()
end
end
 
return workshopsTable[targetWorkshopID]
local workshopID = findWorkshopIDByName(displayName)
if not workshopID then
return nil
end
 
return workshopsTable[workshopID]
end
 
 
 
---
--- Retrieves the description for the workshop specified by its plain language
--- display name.
---
--- Returns nil if the workshop named was not found.
---
---@param displayName string with the plain language name of the workshop
---@return string the in-game description of the specified workshop
function WorkshopsData.getWorkshopDescription(displayName)
 
local workshop = WorkshopsData.getAllDataForWorkshop(displayName)
 
if not workshop then
return nil
end
 
return workshop[INDEX_DESCRIPTION]
end
end


Line 124: Line 395:


---
---
-- Transforms the oldWorkshopsTable returned from CSV processing to be more
--- Retrieves the construction toolbar category for the workshop specified by
-- conducive to member functions looking up data. Essentially, we convert the
--- its plain language display name.
-- text strings into tables with the same base name and an index.
---
--  
--- Returns nil if the workshop named was not found.
-- @param oldRecipeTable the CSV-based table, with a header row then data rows
---
-- @param recipeHeaderLookup the lookup table built from the CSV data
---@param displayName string with the plain language name of the workshop
-- @return a new table for use in looking up recipe data
---@return string the category
function restructureWorkshopTable(oldWorkshopTable, workshopsHeaderLookup)
function WorkshopsData.getWorkshopCategory(displayName)
 
-- New table structure is only data rows, no header row needed. Therefore,
local workshop = WorkshopsData.getAllDataForWorkshop(displayName)
-- the new table is one degree flatter, like this:
 
-- oldWorkshopTable[2][1] contains the same data as newWorkshopTable[1]
if not workshop then
-- (but organized by workshopID instead of by index and with subtables)
return nil
local newWorkshopTable = {}
end
oldWorkshopTable = oldWorkshopTable[DATA_ROWS]
 
return workshop[INDEX_CATEGORY]
-- A few constants we need only in this function.
end
local INDEX_OLD_WORKSHOP_ID = 1
 
local INDEX_OLD_WORKSHOP_NAME = 2
 
local INDEX_OLD_WORKSHOP_DESCRIPTION = 3
 
local INDEX_OLD_WORKSHOP_CATEGORY = 4
---
local INDEX_OLD_WORKSHOP_SIZE_X = 5
--- Retrieves the 2x2 size for the workshop specified by its plain language
local INDEX_OLD_WORKSHOP_SIZE_Y = 6
--- display name, in two return values.
local INDEX_OLD_WORKSHOP_CONSTRUCTION_TIME = 10
---
local INDEX_OLD_WORKSHOP_CITY_SCORE = 11
--- Returns nil if the workshop named was not found.
local INDEX_OLD_WORKSHOP_MOVABLE = 12
---
local INDEX_OLD_WORKSHOP_INITIALLY_ESSENTIAL = 13
---@param displayName string with the plain language name of the workshop
local INDEX_OLD_WORKSHOP_STORAGE = 14
---@return number the X-size of the workshop
---@return number the Y-size of the workshop
for i, oldWorkshop in ipairs(oldWorkshopTable) do
function WorkshopsData.getWorkshopSize(displayName)
 
local newWorkshop = {}
local workshop = WorkshopsData.getAllDataForWorkshop(displayName)
 
-- Copy over the flat information from the old recipe.
if not workshop then
local newWorkshopID = oldWorkshop[INDEX_OLD_WORKSHOP_ID]
return nil
newWorkshop[INDEX_WORKSHOP_ID] = newWorkshopID
end
newWorkshop[INDEX_WORKSHOP_NAME] = oldWorkshop[INDEX_OLD_WORKSHOP_NAME]
 
newWorkshop[INDEX_WORKSHOP_DESCRIPTION] = oldWorkshop[INDEX_OLD_WORKSHOP_DESCRIPTION]
return workshop[INDEX_SIZE_X], workshop[INDEX_SIZE_Y]
newWorkshop[INDEX_WORKSHOP_CATEGORY] = oldWorkshop[INDEX_OLD_WORKSHOP_CATEGORY]
end
newWorkshop[INDEX_WORKSHOP_SIZE_X] = oldWorkshop[INDEX_OLD_WORKSHOP_SIZE_X]
 
newWorkshop[INDEX_WORKSHOP_SIZE_Y] = oldWorkshop[INDEX_OLD_WORKSHOP_SIZE_Y]
 
newWorkshop[INDEX_WORKSHOP_CITY_SCORE] = oldWorkshop[INDEX_OLD_WORKSHOP_CITY_SCORE]
 
newWorkshop[INDEX_WORKSHOP_MOVABLE] = oldWorkshop[INDEX_OLD_WORKSHOP_MOVABLE]
---
newWorkshop[INDEX_WORKSHOP_INITIALLY_ESSENTIAL] = oldWorkshop[INDEX_OLD_WORKSHOP_INITIALLY_ESSENTIAL]
--- Retrieves the goods required for construction for the workshop specified by
newWorkshop[INDEX_WORKSHOP_STORAGE] = oldWorkshop[INDEX_OLD_WORKSHOP_STORAGE]
--- its plain language display name, in a table that looks like this:
newWorkshop[INDEX_WORKSHOP_CONSTRUCTION_TIME] = oldWorkshop[INDEX_OLD_WORKSHOP_CONSTRUCTION_TIME]
---
--- ["requiredGoods"] = {
newWorkshop[INDEX_WORKSHOP_REQUIRED_GOODS] = makeRequiredGoodsSubtable(oldWorkshop, workshopsHeaderLookup)
--- [1] = { ["stackSize"] = 99, ["goodID"] = "good1_ID" },
newWorkshop[INDEX_WORKSHOP_WORKPLACES] = makeWorkplacesSubtable(oldWorkshop, workshopsHeaderLookup)
--- [2] = { ["stackSize"] = 99, ["goodID"] = "good2_ID" },
newWorkshop[INDEX_WORKSHOP_RECIPES] = makeRecipesSubtable(oldWorkshop, workshopsHeaderLookup)
--- [3] = { ... } or missing if fewer
--- }
newWorkshopTable[newWorkshopID] = newWorkshop
---
--- Returns nil if the workshop named was not found.
---
---@param displayName string with the plain language name of the workshop
---@return table of required goods
function WorkshopsData.getWorkshopRequiredGoods(displayName)
 
local workshop = WorkshopsData.getAllDataForWorkshop(displayName)
 
if not workshop then
return nil
end
end
 
return newWorkshopTable
return workshop[INDEX_REQUIRED_GOODS]
end
end


Line 184: Line 465:


---
---
-- Loops through the old workshop, extracting req'd goods and putting them
--- Retrieves the construction time for the workshop specified by its plain
-- into a new subtable. Uses the lookup table from the CSV extraction to
--- language display name.
-- keep the data in the same order.
---
--
--- Returns nil if the workshop named was not found.
-- @param oldWorkshop the workshop from which to extract req'd goods information
---
-- @param workshopsHeaderLookup the lookup table built from the CSV data
---@param displayName string with the plain language name of the workshop
-- @return a subtable with the req'd goods information from oldWorkshop
---@return number of seconds it takes to construct the workshop
function makeRequiredGoodsSubtable(oldWorkshop, workshopsHeaderLookup)
function WorkshopsData.getWorkshopConstructionTime(displayName)
 
-- A few constants we'll need only within this function.
local workshop = WorkshopsData.getAllDataForWorkshop(displayName)
local REQ_GOOD_MAX = 3
 
local LOOKUP_REQ_GOOD_BASE_STRING = "requiredGood"
if not workshop then
return nil
-- A subtable to return
local requiredGoods = {}
for i = 1,REQ_GOOD_MAX do
local oldIndex = workshopsHeaderLookup[LOOKUP_REQ_GOOD_BASE_STRING .. i]
local newGroup = {}
-- Split the digit part into the req'd good stack size and the rest
-- of the text into the req'd good's id.
newGroup[1], newGroup[2] = oldWorkshop[oldIndex]:match(PATTERN_SPLIT_STACK_AND_ID)
table.insert(requiredGoods, newGroup)
end
end
 
return requiredGoods
return workshop[INDEX_CONSTRUCTION_TIME]
end
end


Line 216: Line 486:


---
---
-- Loops through the old workshop, extracting workplaces and putting them
--- Retrieves the city score awarded for the workshop specified by its plain
-- into a new subtable. Uses the lookup table from the CSV extraction to
--- language display name.
-- keep the data in the same order.
---
--
--- Returns nil if the workshop named was not found.
-- @param oldWorkshop the workshop from which to extract workplaces information
---
-- @param workshopsHeaderLookup the lookup table built from the CSV data
---@param displayName string with the plain language name of the workshop
-- @return a subtable with the workplaces information from oldWorkshop
---@return number of points for city score from having the workshop
function makeWorkplacesSubtable(oldWorkshop, workshopsHeaderLookup)
function WorkshopsData.getWorkshopCityScore(displayName)
 
-- A few constants we'll need only within this function.
local workshop = WorkshopsData.getAllDataForWorkshop(displayName)
local WORKPLACE_MAX = 4
 
local LOOKUP_WORKPLACE_BASE_STRING = "workplace"
if not workshop then
return nil
-- A subtable to return
local workplaces = {}
for i = 1,WORKPLACE_MAX do
local oldIndex = workshopsHeaderLookup[LOOKUP_WORKPLACE_BASE_STRING .. i]
workplaces[i] = oldWorkshop[oldIndex]
end
end
 
return workplaces
return workshop[INDEX_CITY_SCORE]
end
end


Line 244: Line 507:


---
---
-- Loops through the old workshop, extracting recipes and putting them
--- Retrieves whether the workshop specified by its plain language display
-- into a new subtable. Uses the lookup table from the CSV extraction to
--- name can be moved, at any cost.
-- keep the data in the same order.
---
--
--- Returns nil if the workshop named was not found.
-- @param oldWorkshop the workshop from which to extract recipes
---
-- @param workshopsHeaderLookup the lookup table built from the CSV data
---@param displayName string with the plain language name of the workshop
-- @return a subtable with the recipes from oldWorkshop
---@return boolean of whether workshop can be moved
function makeRecipesSubtable(oldWorkshop, workshopsHeaderLookup)
function WorkshopsData.isWorkshopMoveable(displayName)
 
-- A few constants we'll need only within this function.
local workshop = WorkshopsData.getAllDataForWorkshop(displayName)
local RECIPE_MAX = 4
 
local LOOKUP_RECIPE_BASE_STRING = "recipe"
if not workshop then
return nil
-- A subtable to return
local recipes = {}
for i=1,RECIPE_MAX do
local oldIndex = workshopsHeaderLookup[LOOKUP_RECIPE_BASE_STRING .. i]
recipes[i] = oldWorkshop[oldIndex]
end
end
 
return recipes
return workshop[INDEX_MOVABLE]
end
end


Line 272: Line 528:


---
---
-- Look up so workshops can be found by their common ID, rather than their
--- Retrieves whether the workshop specified by its plain language display
-- display name, which people are more familiar with.
--- name has an essential starting blueprint, and for a new game profile.
--  
---
-- Uses the display name provided to look up the associated ID for the workshop.
--- Returns nil if the workshop named was not found.
-- Builds a lookup table the first time it's called so it only has to happen
---
-- once.
---@param displayName string with the plain language name of the workshop
--
---@return boolean of whether workshop's blueprint is essential.
-- @param displayName the plain-language name of the workshop to find
function WorkshopsData.isWorkshopInitiallyEssential(displayName)
-- @return the ID of the workshop found, or nil if not found
 
function findWorkshopIDByName(displayName)
local workshop = WorkshopsData.getAllDataForWorkshop(displayName)
 
if not workshopsTable then
if not workshop then
loadData()
return nil
end
end
 
local foundWorkshopID = nil
return workshop[INDEX_ESSENTIAL]
-- Decide whether we need to traverse the big table. If this isn't the first
end
-- time this method is called, we won't have to build the table again, we
 
-- can just look it up.
 
if not workshopNameToWorkshopID then
 
---
workshopNameToWorkshopID = {}
--- Retrieves the storage capacity of the workshop specified by its plain
--- language display name.
for workshopID, workshop in pairs(workshopsTable) do
---
--- Returns nil if the workshop named was not found.
if not foundWorkshopID and workshop[INDEX_WORKSHOP_NAME] == displayName then
---
-- Found it, but keep traversing to build the rest of the map.
---@param displayName string with the plain language name of the workshop
foundWorkshopID = workshopID
---@return number representing the storage capacity of the workshop
end
function WorkshopsData.getWorkshopStorage(displayName)
 
workshopNameToWorkshopID[workshop[INDEX_WORKSHOP_NAME]] = workshopID
local workshop = WorkshopsData.getAllDataForWorkshop(displayName)
end
 
else
if not workshop then
-- From the lookup table.
return nil
foundWorkshopID = workshopNameToWorkshopID[displayName]
end
end
 
return foundWorkshopID
return workshop[INDEX_STORAGE_CAP]
end
end


Line 315: Line 570:


---
---
-- Retrieves the name and icon filename for a workshop.
--- Retrieves the workplaces at the workshop specified by its plain language
--
--- display name, in a table that looks like this:
-- @param workshopID the ID of the workshop to look up
---
-- @return display name, icon filename
--- ["workplaces"] = {
function WorkshopsData.getWorkshopNameAndIcon(workshopID)
--- [1] = "Any",
--- [2] = "Any",
if not workshopsTable then
--- [3] = "Any",
loadData()
--- [4] = ... or missing if fewer
--- }
---
--- The number of workplaces can easily be found with #workplaces.
---
--- Returns nil if the workshop named was not found.
---
---@param displayName string with the plain language name of the workshop
---@return table of workplaces
function WorkshopsData.getWorkshopWorkplaces(displayName)
 
local workshop = WorkshopsData.getAllDataForWorkshop(displayName)
 
if not workshop then
return nil
end
end
 
local workshop = workshopsTable[workshopID]
return workshop[INDEX_WORKPLACES]
end
 
 
 
---
--- Retrieves the recipes possible at the workshop 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 workshop named was not found.
---
---@param displayName string with the plain language name of the workshop
---@return table of recipe IDs
function WorkshopsData.getWorkshopRecipes(displayName)
 
local workshop = WorkshopsData.getAllDataForWorkshop(displayName)
 
if not workshop then
if not workshop then
error("ID for workshop not found to look up name and icon: " .. workshopID)
return nil
end
end
 
return workshop[INDEX_WORKSHOP_NAME], workshop[INDEX_WORKSHOP_NAME] .. "_icon"
return workshop[INDEX_RECIPES]
end
end




---
--- Retrieves the icon filename for the workshop specified by its plain
--- language display name.
---
--- Returns nil if the workshop named was not found.
---
---@param displayName string with the plain language name of the workshop
---@return string the workshop's icon filename, including the extension
function WorkshopsData.getWorkshopIcon(displayName)
local workshop = WorkshopsData.getAllDataForWorkshop(displayName)
if not workshop then
return nil
end
-- the base string of the icon is the ID. it has to be not nil to
-- concatenate
if workshop[INDEX_ID] then
return workshop[INDEX_ID] .. "_icon.png"
else
return nil
end
end
--endregion


return WorkshopsData
return WorkshopsData

Revision as of 04:29, 18 November 2023

Overview

Module for compiling workshops (or production buildings as they are called in-game) 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.

Usage

The standard way of using this module is a call like the following:

iconFilename = WorkshopsData.getWorkshopIcon(workshopName)

This will return a string corresponding to the filename of the icon of the workshop, including the .png extension. It is preferable to call getter methods with the name of the workshop rather than retrieving the entire record for the workshop, so that your code stays protected from variations in this module. The getter methods are all called with the plain-language name of the workshop, as spelled in the game.

longDescription = WorkshopsData.getWorkshopDescription(workshopName)
constructionCategory = WorkshopsData.getWorkshopCategory(workshopName)
sizeX, sizeY = WorkshopsData.getWorkshopSize(workshopName)
requiredGoodID, stackSize = WorkshopsData.getWorkshopRequiredGood(workshopName, requirementIndex)
timeSeconds = WorkshopsData.getWorkshopConstructionTime(workshopName)
cityScore = WorkshopsData.getWorkshopCityScore(workshopName)
isMovable = WorkshopsData.isWorkshopMoveable(workshopName)
isInitiallyEssential = WorkshopsData.isWorkshopInitiallyEssential(workshopName)
storageCap = WorkshopsData.getWorkshopStorage(workshopName)
workplaces = WorkshopsData.getWorkshopNumberOfWorkplaces(workshopName)
recipesTable = WorkshopsData.getWorkshopRecipe(workshopName, recipeIndex)
iconFilename = WorkshopsData.getWorkshopIcon(workshopName)

And the following getter methods are all called with the workshop's ID. You will have an ID instead of a display name when dealing with other game data like, recipes, goods, etc.

name = WorkshopsData.getWorkshopNameByID(workshopID)
iconFilename = WorkshopsData.getWorkshopIconByID(workshopID)

The list of workshops that can produce a particular recipe ID can be retrieved with the following method

workshopNamesTable = WorkshopsData.getWorkshopNamesWithRecipeID(recipeID)

There are methods to retrieve the lists of required goods, workplaces, and recipes, but it is advised to instead use the getter methods getWorkshopRequiredGood, getWorkshopNumberOfWorkplaces, and getWorkshopRecipe with an index desired, which is good for loops. If you must get the tables, there are three "getAll" methods:

requiredGoodsTable = WorkshopsData.getAllWorkshopRequiredGoods(workshopName)
workplacesTable = WorkshopsData.getAllWorkshopWorkplaces(workshopName)
recipesTable = WorkshopsData.getAllWorkshopRecipes(workshopName)

As a last resort, or if you need to transform the data structure, you can call the method getAllDataForWorkshop(displayName) or getAllDataForWorkshopByID(workshopID). This returns a whole record from the data table corresponding to the requested display name.

The data table for workshops has the following structure:

workshopsTable = {
	["workshop1_ID"] = {
		["id"] = "workshop1_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
		["storage"] = 99, capacity
		["workplaces"] = {
			[1] = "Any",
			[2] = "Any",
			[3] = "Any",
			[4] = ... representing the number of workplaces, or missing if fewer
		},
		["recipes"] = {
			[1] = "recipe1_ID",
			[2] = "recipe2_ID",
			[3] = "recipe3_ID",
			[4] = ... or missing if fewer
		}
	},
	["workshop2_ID"] = {
		...
	},
	["workshop3_ID"] = {
		...
	},
	...
}

---
--- Module for compiling workshops (or production buildings as they are called
--- in-game) 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:
---
--- iconFilename = WorkshopsData.getWorkshopIcon(workshopName)
---
--- This will return a string corresponding to the filename of the icon of the
--- workshop. It is preferable to call getter methods with the name of the good
--- rather than retrieving the entire record for the good, so that your code
--- stays protected from variations in this module. The getter methods are all
--- called with the plain-language name of the good, as spelled in the game.
---
--- * longDescription = WorkshopsData.getWorkshopDescription(workshopName)
--- * constructionCategory = WorkshopsData.getWorkshopCategory(workshopName)
--- * sizeX, sizeY = WorkshopsData.getWorkshopSize(workshopName)
--- * requiredGoodsTable = WorkshopsData.getWorkshopRequiredGoods(workshopName)
--- * timeSeconds = WorkshopsData.getWorkshopConstructionTime(workshopName)
--- * cityScore = WorkshopsData.getWorkshopCityScore(workshopName)
--- * isMovable = WorkshopsData.isWorkshopMoveable(workshopName)
--- * isInitiallyEssential = WorkshopsData.isWorkshopInitiallyEssential(workshopName)
--- * storageCap = WorkshopsData.isWorkshopInitiallyEssential(workshopName)
--- * workplacesTable = WorkshopsData.getWorkshopWorkplaces(workshopName)
--- * recipesTable = WorkshopsData.getWorkshopRecipes(workshopName)
--- * iconFilename = WorkshopsData.getWorkshopIcon(workshopName)
---
--- As a last resort, or if you need to transform the data structure, you can
--- call the method getAllDataForWorkshop(displayName). This returns a whole
--- record from the data table corresponding to the requested display name.
---
--- The data table for workshops has the following structure:
---
--- workshopsTable = {
--- 	["workshop1_ID"] = {
--- 		["id"] = "workshop1_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
--- 		["storage"] = 99, capacity
--- 		["workplaces"] = {
--- 			[1] = "Any",
--- 			[2] = "Any",
--- 			[3] = "Any",
--- 			[4] = ... representing the number of workplaces, or missing if fewer
--- 		},
--- 		["recipes"] = {
--- 			[1] = "recipe1_ID",
--- 			[2] = "recipe2_ID",
--- 			[3] = "recipe3_ID",
--- 			[4] = ... or missing if fewer
--- 		}
--- 	},
--- 	["workshop2_ID"] = {
--- 		...
--- 	},
---		["workshop3_ID"] = {
--- 		...
--- 	},
--- 	...
--- }
---
--- @module WorkshopsData
local WorkshopsData = {}



local CsvUtils = require("Module:CsvUtils")



--region Private member variables

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

--- Lookup map. Built once and reused on all subsequent calls within this
--- session, like this: table[displayName] = workshopID
local mapNamesToIDs

--endregion



--region Private constants

local DATA_TEMPLATE_NAME = "Template:Workshops_csv"

local HEADER_ROW = 1
local DATA_ROWS = 2

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_CITY_SCORE = "cityScore"
local INDEX_MOVABLE = "movable"
local INDEX_ESSENTIAL = "initiallyEssential"
local INDEX_STORAGE_CAP = "storage"
local INDEX_CONSTRUCTION_TIME = "constructionTime"
local INDEX_REQUIRED_GOODS = "requiredGoods"
local INDEX_WORKPLACES = "workplaces"
local INDEX_RECIPES = "recipes"

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

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

--endregion



--region Private methods

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

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

	local number1, id1 = originalWorkshop[requiredIndex1]:match(PATTERN_SPLIT_STACK_AND_ID)
	local number2, id2 = originalWorkshop[requiredIndex2]:match(PATTERN_SPLIT_STACK_AND_ID)
	local number3, id3 = originalWorkshop[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] = number1, [INDEX_CONSTRUCTION_GOODS_GOOD_ID] = id1 } or nil,
		number2 and id2 and { [INDEX_CONSTRUCTION_GOODS_STACK_SIZE] = number2, [INDEX_CONSTRUCTION_GOODS_GOOD_ID] = id2 } or nil,
		number3 and id3 and { [INDEX_CONSTRUCTION_GOODS_STACK_SIZE] = number3, [INDEX_CONSTRUCTION_GOODS_GOOD_ID] = id3 } or nil
	}

	return requiredGoods
end



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

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

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

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

	-- A constant we'll need only within this function.
	local LOOKUP_RECIPE_BASE_STRING = "recipe"

	-- Copy the originals directly into a subtable.
	local recipeIndex1 = workshopsHeaderLookup[LOOKUP_RECIPE_BASE_STRING .. "1"]
	local recipeIndex2 = workshopsHeaderLookup[LOOKUP_RECIPE_BASE_STRING .. "2"]
	local recipeIndex3 = workshopsHeaderLookup[LOOKUP_RECIPE_BASE_STRING .. "3"]
	local recipeIndex4 = workshopsHeaderLookup[LOOKUP_RECIPE_BASE_STRING .. "4"]

	local recipe1 = originalWorkshop[recipeIndex1]
	local recipe2 = originalWorkshop[recipeIndex2]
	local recipe3 = originalWorkshop[recipeIndex3]
	local recipe4 = originalWorkshop[recipeIndex4]

	local recipes = {
		(recipe1 ~= "" and recipe1) or nil,
		(recipe2 ~= "" and recipe2) or nil,
		(recipe3 ~= "" and recipe3) or nil,
		(recipe4 ~= "" and recipe4) or nil
	}

	return recipes
end



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

	-- A few constants we'll need only within this function.
	local HEADER_REQUIRED_GOODS_BASE = "requiredGood"
	local HEADER_WORKPLACE_BASE = "workplace"
	local HEADER_RECIPES_BASE = "recipe"

	mapNamesToIDs = {}

	local newWorkshopTable = {}
	for _, originalWorkshop in ipairs (originalWorkshopsTable[DATA_ROWS]) do

		-- Copy over the content, mapping unhelpful indexes into headers keys.
		local newWorkshop = {}
		for index, key in pairs(originalWorkshopsTable[HEADER_ROW]) do

			-- Convert string versions of true/false to actual booleans
			if key == INDEX_MOVABLE or key == INDEX_ESSENTIAL then
				newWorkshop[key] = originalWorkshop[index] == "True"
			else
				-- Convert a some of the strings to subtables. But only once
				-- per group of related strings.
				if key:sub(1, #HEADER_REQUIRED_GOODS_BASE) == HEADER_REQUIRED_GOODS_BASE then
					if not newWorkshop[INDEX_REQUIRED_GOODS] then
						newWorkshop[INDEX_REQUIRED_GOODS] = makeRequiredGoodsSubtable(originalWorkshop, workshopsHeaderLookup)
					end
				else
					if key:sub(1, #HEADER_WORKPLACE_BASE) == HEADER_WORKPLACE_BASE then
						if not newWorkshop[INDEX_WORKPLACES] then
							newWorkshop[INDEX_WORKPLACES] = makeWorkplacesSubtable(originalWorkshop, workshopsHeaderLookup)
						end
					else
						if key:sub(1, #HEADER_RECIPES_BASE) == HEADER_RECIPES_BASE then
							if not newWorkshop[INDEX_RECIPES] then
								newWorkshop[INDEX_RECIPES] = makeRecipesSubtable(originalWorkshop, workshopsHeaderLookup)
							end
						else
							-- Everything else can be directly copied over.
							newWorkshop[key] = originalWorkshop[index]
						end
					end
				end
			end
		end

		newWorkshopTable[ newWorkshop[INDEX_ID] ] = newWorkshop

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

	return newWorkshopTable
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.
function loadData()

	-- Utility module retrieves the data as basic, flat lua tables.
	local originalWorkshopsTable, workshopsHeaderLookup = CsvUtils.luaTableFromCSV(CsvUtils.extractCSV(DATA_TEMPLATE_NAME))

	-- Now restructure to be more conducive for data about goods.
	workshopsTable = restructureWorkshopsTable(originalWorkshopsTable, workshopsHeaderLookup)
end



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

	if not workshopsTable then
		loadData()
	end

	return mapNamesToIDs[displayName]
end

--endregion



--region Public methods

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

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

	if not workshopsTable then
		WorkshopsData.loadData()
	end

	local workshopID = findWorkshopIDByName(displayName)
	if not workshopID then
		return nil
	end

	return workshopsTable[workshopID]
end



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

	local workshop = WorkshopsData.getAllDataForWorkshop(displayName)

	if not workshop then
		return nil
	end

	return workshop[INDEX_DESCRIPTION]
end



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

	local workshop = WorkshopsData.getAllDataForWorkshop(displayName)

	if not workshop then
		return nil
	end

	return workshop[INDEX_CATEGORY]
end



---
--- Retrieves the 2x2 size for the workshop specified by its plain language
--- display name, in two return values.
---
--- Returns nil if the workshop named was not found.
---
---@param displayName string with the plain language name of the workshop
---@return number the X-size of the workshop
---@return number the Y-size of the workshop
function WorkshopsData.getWorkshopSize(displayName)

	local workshop = WorkshopsData.getAllDataForWorkshop(displayName)

	if not workshop then
		return nil
	end

	return workshop[INDEX_SIZE_X], workshop[INDEX_SIZE_Y]
end



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

	local workshop = WorkshopsData.getAllDataForWorkshop(displayName)

	if not workshop then
		return nil
	end

	return workshop[INDEX_REQUIRED_GOODS]
end



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

	local workshop = WorkshopsData.getAllDataForWorkshop(displayName)

	if not workshop then
		return nil
	end

	return workshop[INDEX_CONSTRUCTION_TIME]
end



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

	local workshop = WorkshopsData.getAllDataForWorkshop(displayName)

	if not workshop then
		return nil
	end

	return workshop[INDEX_CITY_SCORE]
end



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

	local workshop = WorkshopsData.getAllDataForWorkshop(displayName)

	if not workshop then
		return nil
	end

	return workshop[INDEX_MOVABLE]
end



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

	local workshop = WorkshopsData.getAllDataForWorkshop(displayName)

	if not workshop then
		return nil
	end

	return workshop[INDEX_ESSENTIAL]
end



---
--- Retrieves the storage capacity of the workshop specified by its plain
--- language display name.
---
--- Returns nil if the workshop named was not found.
---
---@param displayName string with the plain language name of the workshop
---@return number representing the storage capacity of the workshop
function WorkshopsData.getWorkshopStorage(displayName)

	local workshop = WorkshopsData.getAllDataForWorkshop(displayName)

	if not workshop then
		return nil
	end

	return workshop[INDEX_STORAGE_CAP]
end



---
--- Retrieves the workplaces at the workshop specified by its plain language
--- display name, in a table that looks like this:
---
--- ["workplaces"] = {
--- 	[1] = "Any",
--- 	[2] = "Any",
--- 	[3] = "Any",
--- 	[4] = ... or missing if fewer
--- }
---
--- The number of workplaces can easily be found with #workplaces.
---
--- Returns nil if the workshop named was not found.
---
---@param displayName string with the plain language name of the workshop
---@return table of workplaces
function WorkshopsData.getWorkshopWorkplaces(displayName)

	local workshop = WorkshopsData.getAllDataForWorkshop(displayName)

	if not workshop then
		return nil
	end

	return workshop[INDEX_WORKPLACES]
end



---
--- Retrieves the recipes possible at the workshop 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 workshop named was not found.
---
---@param displayName string with the plain language name of the workshop
---@return table of recipe IDs
function WorkshopsData.getWorkshopRecipes(displayName)

	local workshop = WorkshopsData.getAllDataForWorkshop(displayName)

	if not workshop then
		return nil
	end

	return workshop[INDEX_RECIPES]
end



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

	local workshop = WorkshopsData.getAllDataForWorkshop(displayName)

	if not workshop then
		return nil
	end

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

--endregion

return WorkshopsData