Module:WorkshopsData

--- --- 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, 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) --- * requiredGoodsTable = WorkshopsData.getWorkshopRequiredGoods(workshopName) --- * timeSeconds = WorkshopsData.getWorkshopConstructionTime(workshopName) --- * cityScore = WorkshopsData.getWorkshopCityScore(workshopName) --- * isMovable = WorkshopsData.isWorkshopMovable(workshopName) --- * isInitiallyEssential = WorkshopsData.isWorkshopInitiallyEssential(workshopName) --- * storageCap = WorkshopsData.getWorkshopStorage(workshopName) --- * workplacesTable = WorkshopsData.getWorkshopWorkplaces(workshopName) --- * recipesTable = WorkshopsData.getWorkshopRecipes(workshopName) --- * 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) --- --- 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 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. 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 workshopID string ID of the workshop --- @return table containing the data for the specified workshop with key-value pairs, or nil if not found function WorkshopsData.getAllDataForWorkshopByID(workshopID)

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

if not workshopsTable then loadData end

return workshopsTable[workshopID] end

--- --- 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.") end

if not workshopsTable then loadData end

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

return workshopsTable[workshopID] end

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

local workshop = WorkshopsData.getAllDataForWorkshop(displayName)

if not workshop then return nil end

return workshop[INDEX_ID] 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 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 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 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 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 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 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 the plain language name of the workshop ---@return boolean of whether workshop can be moved function WorkshopsData.isWorkshopMovable(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 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 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 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 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 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

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

-- Rather than getting it directly from workshopsTable here, get it via -- this other method that has additional error checking. local workshop = WorkshopsData.getAllDataForWorkshopByID(workshopID)

if not workshop then return nil end

return workshop[INDEX_NAME] end

--- --- Retrieves the icon filename for the workshop specified by its ID. --- --- Returns nil if the workshop named was not found. --- ---@param workshopID string the ID of the workshop ---@return string the workshop's icon filename, including the extension function WorkshopsData.getWorkshopIconByID(workshopID)

-- Rather than getting it directly from workshopsTable here, get it via -- this other method that has additional error checking. local workshop = WorkshopsData.getAllDataForWorkshopByID(workshopID)

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