Module:RecipeData: Difference between revisions

From Against the Storm Official Wiki
(Undo revision 2731 by 76561197969631737 (talk))
Tags: Undo Reverted
(yet another typo in the new method; it was hard to unit test :/)
 
(4 intermediate revisions by the same user not shown)
Line 367: Line 367:
return goodsTable[targetIngredientID], ingredMatches
return goodsTable[targetIngredientID], ingredMatches
end
end
---
-- Retrieve all data for the specified workshop. This was originally the main
-- reason to have a separate WorkshopData module, but it still required most of
-- the RecipeData functionality (to identify the content of recipes, or even
-- just which products the recipes produced), so this is moved over here, and
-- there doesn't seem to be an independent need for a WorkshopData module right
-- now.
--
-- @param workshopName plain language name of the workshop
-- @return a table containing the data for the specified workshop
function RecipeData.getAllDataForWorkshop(workshopName)
if not workshopTable then
loadRecipes()
end
local targetWorkshopID = findWorkshopIDByName(workshopName)
if not targetWorkshopID then
error("No building found. Please check spelling and any punctuation like an apostrophe: " .. workshopName)
end
return workshopTable[targetWorkshopID]
end




Line 508: Line 535:
-- construction time
-- construction time
-- required goods {
-- required goods {
-- required good #1 stack size
-- #1 { stack size, id }
-- required good #1 id
-- #2 { stack size, id }
-- required good #2 stack size
-- #3 { stack size, id }
-- required good #2 id
-- required good #3 stack size
-- required good #3 id
-- }
-- }
-- workplaces {
-- workplaces {
Line 600: Line 624:
-- A subtable to return
-- A subtable to return
local requiredGoods = {}
local requiredGoods = {}
-- We'll have to add twice as many new fields, so we need a separate
-- counter.
local k = 1
for i = 1,REQ_GOOD_MAX do
for i = 1,REQ_GOOD_MAX do
local oldIndex = workshopsHeaderLookup[LOOKUP_REQ_GOOD_BASE_STRING .. i]
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  
-- Split the digit part into the req'd good stack size and the rest  
-- of the text into the req'd good's id.
-- of the text into the req'd good's id.
requiredGoods[k], requiredGoods[k+1] = oldWorkshop[oldIndex]:match(PATTERN_SPLIT_STACK_AND_ID)
newGroup[1], newGroup[2] = oldWorkshop[oldIndex]:match(PATTERN_SPLIT_STACK_AND_ID)
k = k + 2 -- move +2 for each original +1
table.insert(requiredGoods, newGroup)
end
end
Line 866: Line 887:
-- @param goodID the ID of the good to look up
-- @param goodID the ID of the good to look up
-- @return display name, icon filename
-- @return display name, icon filename
function getGoodNameAndIcon(goodID)
function RecipeData.getGoodNameAndIcon(goodID)
if not recipeTable or not workshopTable or not goodsTable then
if not recipeTable or not workshopTable or not goodsTable then
Line 879: Line 900:
return good[INDEX_GOOD_NAME], good[INDEX_GOOD_ICON_FILENAME]
return good[INDEX_GOOD_NAME], good[INDEX_GOOD_ICON_FILENAME]
end
---
-- Retrieves the name and icon filename for a workshop.
--
-- @param workshopID the ID of the workshop to look up
-- @return display name, icon filename
function RecipeData.getWorkshopNameAndIcon(workshopID)
if not workshopTable then
loadRecipes()
end
local workshop = workshopTable[workshopID]
if not workshop then
error("ID for workshop not found to look up name and icon: " .. workshopID)
end
return workshop[INDEX_WORKSHOP_NAME], workshop[INDEX_WORKSHOP_NAME] .. "_icon"
end
end



Latest revision as of 14:23, 10 November 2023

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

---
-- Module for compiling recipe information from wiki data sources
--
-- @module RecipeData
local RecipeData = {}

---
-- Dependencies
---
local CsvUtils = require("Module:CsvUtils")

---
-- Constants for this module
---
local RECIPE_DATA_TEMPLATE_NAME = "Template:Workshops_Recipes_csv"
local WORKSHOPS_DATA_TEMPLATE_NAME = "Template:Workshops_csv"
local GOODS_DATA_TEMPLATE_NAME = "Template:Goods_csv"

local HEADER_ROW = 1
local DATA_ROWS = 2

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

local INDEX_RECIPE_ID = 1
local INDEX_RECIPE_GRADE = 2
local INDEX_RECIPE_PRODTIME = 3
local INDEX_RECIPE_PRODUCT_STACK_SIZE = 4
local INDEX_RECIPE_PRODUCT_ID = 5
local INDEX_RECIPE_INGREDIENTS = 6

local INDEX_RECIPE_INGREDIENT_OPTION_GOODID = 2

local INDEX_WORKSHOP_ID = 1
local INDEX_WORKSHOP_NAME = 2
local INDEX_WORKSHOP_DESCRIPTION = 3
local INDEX_WORKSHOP_CATEGORY = 4
local INDEX_WORKSHOP_SIZE_X = 5
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 INDEX_GOOD_ID = 1
local INDEX_GOOD_NAME = 2
local INDEX_GOOD_DESCRIPTION = 3
local INDEX_GOOD_CATEGORY = 4
local INDEX_GOOD_EATABLE = 5
local INDEX_GOOD_CAN_BE_BURNED = 6
local INDEX_GOOD_BURNING_TIME = 7
local INDEX_GOOD_TRADING_SELL_VALUE = 8
local INDEX_GOOD_TRADING_BUY_VALUE = 9
local INDEX_GOOD_ICON_FILENAME = 10



---
-- Private member variables
---

-- Main data tables. Populated from CSV data and organized better for Lua.

-- like this:  table[recipeID] = table containing recipe data
local recipeTable

-- like this:  table[workshopID] = table containing workshop data
local workshopTable

-- like this: table[goodID] = table containing good data
local goodsTable

-- Lookup maps. Built once and reused on subsequent calls

-- like this:  table[recipeID] = { workshopID, workshopID, workshopID }
local recipeIDToWorkshopID

-- like this:  table[display name] = goodID
local goodsNameToGoodID

-- like this: table[display name] = workshopID
local workshopNameToWorkshopID



---
-- Data loader function. Calls the data templates, restructures the data to be
-- useful for getter methods, and makes a merge table.
--
-- This method is called the first time any of the actual template methods are
-- invoked and they see that the data tables are nil. This method populates
-- them and reorganizes them for easier use (and easier code).
function loadRecipes()
	
	-- Use the CSV utility module to load the data templates we need for 
	-- recipes, which are the recipes themselves and the workshops (production) 
	-- workshops.
	local originalRecipeTable, recipeHeaderLookup = CsvUtils.luaTableFromCSV(CsvUtils.extractCSV(RECIPE_DATA_TEMPLATE_NAME))
	local originalWorkshopTable, workshopsHeaderLookup = CsvUtils.luaTableFromCSV(CsvUtils.extractCSV(WORKSHOPS_DATA_TEMPLATE_NAME))
	local originalGoodsTable, goodsHeaderLookup = CsvUtils.luaTableFromCSV(CsvUtils.extractCSV(GOODS_DATA_TEMPLATE_NAME))
	
	-- Now restructure the tables so subtables can be passed to member functions 
	-- for cleaner code.
	recipeTable = restructureRecipeTable(originalRecipeTable, recipeHeaderLookup)
	workshopTable = restructureWorkshopTable(originalWorkshopTable, workshopsHeaderLookup)
	goodsTable = restructureGoodsTable(originalGoodsTable, goodsHeaderLookup)
end



---
-- Retrieve all recipes that result in the specified product, and the buildings
-- that produce them. With the following structure:
-- 
-- table of matches {
-- 		match[recipeID] between recipe and workshops = {
--			recipe data { ... }
--			workshops that make it {
--				workshop data { ... }
--				workshop data { ... }
--				workshop data { ... }
--			}
--		}
--		match[recipeID] between recipe and workshops = {
--			...
-- 		}
-- }
-- 
-- @param productName plain language name of the product (good)
-- @return all recipes that result in the product, and all workshops that
-- produce those recipes, in a nested table
function RecipeData.getAllRecipesForProduct(productName)
	
	if not recipeTable or not workshopTable or not goodsTable then
		loadRecipes()
	end
	
	local targetProductID = findGoodIDByName(productName)
	if not targetProductID then
		error("No product found. Please check spelling and any punctuation like an apostrophe: " .. productName)
	end
	
	-- First find all the relevant recipes.
	local foundRecipeIDs = {}
	for recipeID, recipe in pairs(recipeTable) do
	
		if targetProductID == recipe[INDEX_RECIPE_PRODUCT_ID] then
		
			table.insert(foundRecipeIDs, recipeID)
		end
	end
	
	-- Now run the found recipes to get the workshops.
	local foundWorkshopIDs = findWorkshopsWithAnyOfTheseRecipes(foundRecipeIDs)
	
	-- Build the nested return table
	foundRecipes = {}
	for recipeID, listOfWorkshopIDs in pairs(foundWorkshopIDs) do
		
		-- Make a new table to store the match between the recipe and the 
		-- buildings that make it
		local match = {}
		table.insert(match, recipeTable[recipeID])
		
		local workshops = {}
		for i, workshopID in pairs(listOfWorkshopIDs) do
			
			table.insert(workshops, workshopTable[workshopID])
		end
		
		table.insert(match, workshops)
		foundRecipes[recipeID] = match
	end
	
	return foundRecipes
end



---
-- Retrieve one specified recipe at the specified workshop. Returns an error if
-- no such combination exists. Two return values:
-- 
-- table of matches {
-- 		match[recipeID] between recipe and workshops = {
--			recipe data { ... }
--			workshops that make it {
--				workshop data { ... }
--			}
-- 		}
-- }
-- 
-- @param productName plain language name of the product (good)
-- @param workshopName plain language name of the workshop (building)
-- @return the requested recipe and workshop tables, if the combination exist
function RecipeData.getOneRecipeAtBuilding(productName, workshopName)
	
	if not recipeTable or not workshopTable or not goodsTable then
		loadRecipes()
	end
	
	local targetProductID = findGoodIDByName(productName)
	if not targetProductID then
		error("No product found. Please check spelling and any punctuation like an apostrophe: " .. productName)
	end
	
	local targetWorkshopID = findWorkshopIDByName(workshopName)
	if not targetWorkshopID then
		error("No building found. Please check spelling and any punctuation like an apostrophe: " .. workshopName)
	end
	
	local targetWorkshop = workshopTable[targetWorkshopID]
	
	local targetRecipeID = nil
	for _, recipeID in ipairs(targetWorkshop[INDEX_WORKSHOP_RECIPES]) do
		
		if recipeID ~= "" and targetProductID == recipeTable[recipeID][INDEX_RECIPE_PRODUCT_ID] then
			targetRecipeID = recipeID
			break
		end
	end
	
	if not targetRecipeID then
		error("No combination exists. Please check spelling and punctuation (like an apostrophe) for this recipe and building: " .. productName .. " and " .. workshopName)
	end
	
	local tableToReturn = {
		[targetRecipeID] = {
			recipeTable[targetRecipeID],
			{
				workshopTable[targetWorkshopID]
			}
		}
	}
	
	return tableToReturn
end



---
-- Retrieve all the recipes that the specified workshop can produce, with the
-- following structure:
-- 
-- workshop,
-- table of matches {
-- 		match[recipeID] between recipe and workshops = {
--			recipe data { ... },
--			workshops that make it {
--				workshop data { ... }
--			}
--		},
--		match[recipeID] between recipe and workshops = {
--			recipe data { ... },
--			workshops that make it {
--				workshop data { ... }
--			}
-- 		}
-- }
--
-- @param workshopName the plain language display name of the workshop
-- @return list of recipes at that building
function RecipeData.getAllRecipesAtBuilding(workshopName)
	
	if not recipeTable or not workshopTable or not goodsTable then
		loadRecipes()
	end
	
	local targetWorkshopID = findWorkshopIDByName(workshopName)
	if not targetWorkshopID then
		error("No building found. Please check spelling and any punctuation like an apostrophe: " .. workshopName)
	end
	
	local targetWorkshop = workshopTable[targetWorkshopID]
	-- Reuse this subtable for each recipe.
	local nestedWorkshopList = {}
	table.insert(nestedWorkshopList, targetWorkshop)
	
	local matches = {}
	for _, recipeID in ipairs(targetWorkshop[INDEX_WORKSHOP_RECIPES]) do
		
		if recipeID ~= "" then
		
			local newMatch = {}
			table.insert(newMatch,recipeTable[recipeID])
			table.insert(newMatch, nestedWorkshopList)
			
			matches[recipeID] = newMatch
		end
	end
	
	return targetWorkshop, matches
end



---
-- Retrieves all recipes for which the specified good is an ingredient, with the
-- following structure:
-- 
-- ingredient good data { ... },
-- table of matches {
-- 		match[recipeID] between recipe and workshops = {
--			recipe data { ... }
--			workshops that make it {
--				workshop data { ... }
--				workshop data { ... }
--				workshop data { ... }
--			}
--		}
--		match[recipeID] between recipe and workshops = {
--			...
-- 		}
-- }
-- 
-- @param ingredientName the plain language name of the good
-- @return a table of matches
function RecipeData.getAllRecipesWithIngredient(ingredientName)
	
	if not recipeTable or not workshopTable or not goodsTable then
		loadRecipes()
	end
	
	local targetIngredientID = findGoodIDByName(ingredientName)
	if not targetIngredientID then
		error("No ingredient found. Please check spelling and any punctuation like an apostrophe: " .. ingredientName)
	end
	
	local foundRecipeIDs = {}
	for recipeID, recipe in pairs(recipeTable) do
		
		local match
		for _, optionGroup in ipairs(recipe[INDEX_RECIPE_INGREDIENTS]) do
			for _, option in ipairs (optionGroup) do
			
				-- Compare the good id, no need to look at stack size
				if option[INDEX_RECIPE_INGREDIENT_OPTION_GOODID] == targetIngredientID then
					table.insert(foundRecipeIDs, recipeID)
				end
			end
		end
	end
	
	-- Expand the table to populate workshop IDs under each recipe ID
	local matches = findWorkshopsWithAnyOfTheseRecipes(foundRecipeIDs)
	
	-- Now build the nested table to return with the data
	local ingredMatches = {}
	for recipeID, match in pairs(matches) do
	
		newMatch = {}
		table.insert(newMatch, recipeTable[recipeID])
		
		matchWorkshops = {}
		for _, workshopID in ipairs(match) do
		
			table.insert(matchWorkshops, workshopTable[workshopID])
		end
		table.insert(newMatch, matchWorkshops)
		
		ingredMatches[recipeID] = newMatch
	end
	
	return goodsTable[targetIngredientID], ingredMatches
end



---
-- Retrieve all data for the specified workshop. This was originally the main
-- reason to have a separate WorkshopData module, but it still required most of
-- the RecipeData functionality (to identify the content of recipes, or even 
-- just which products the recipes produced), so this is moved over here, and
-- there doesn't seem to be an independent need for a WorkshopData module right
-- now.
-- 
-- @param workshopName plain language name of the workshop
-- @return a table containing the data for the specified workshop
function RecipeData.getAllDataForWorkshop(workshopName)
	
	if not workshopTable then
		loadRecipes()
	end
	
	local targetWorkshopID = findWorkshopIDByName(workshopName)
	if not targetWorkshopID then
		error("No building found. Please check spelling and any punctuation like an apostrophe: " .. workshopName)
	end
	
	return workshopTable[targetWorkshopID]
end




---
-- Transforms the oldRecipeTable returned from CSV processing to be more 
-- conducive to member functions looking up data. Essentially, we convert the 
-- text strings into tables with the same base name and an index.
-- 
-- Each item in newRecipeTable[i] looks like this:
-- recipe {
-- 		id
--		efficiency grade
--		production time
--		product stack size
--		product good's id
--		ingredients {
--			options for ingredient #1 {
--				{ option #1 good's stack size, option #1 good's id }
--				{ option #2 good's stack size, option #2 good's id }
--				{ option #3 good's stack size, option #3 good's id }
--				etc.
--			}
--			options for ingredient #2 {
--				same as above
--			}
--			options for ingredient #3 {
--				same as above
--			}
--		}
-- }
-- 
-- @param oldRecipeTable the CSV-based table, with a header row then data rows
-- @param recipeHeaderLookup the lookup table built from the CSV data
-- @return a new table for use in looking up recipe data
function restructureRecipeTable(oldRecipeTable, recipeHeaderLookup)
	
	-- New table structure is only data rows, no header row needed. Therefore, 
	-- the new table is one degree flatter, like this:
	-- oldRecipeTable[2][1] contains the same data as newRecipeTable[1]
	-- (but with subtables)
	local newRecipeTable = {}
	oldRecipeTable = oldRecipeTable[DATA_ROWS]
	
	-- A few constants we need only in this function
	local INDEX_OLD_RECIPE_ID = 1
	local INDEX_OLD_RECIPE_GRADE = 2
	local INDEX_OLD_RECIPE_PRODTIME = 3
	local INDEX_OLD_RECIPE_PRODUCT = 4
	
	for i, oldRecipe in ipairs(oldRecipeTable) do
		
		local newRecipe = {}
		
		-- Copy over the flat information from the old recipe.
		local newRecipeID = oldRecipe[INDEX_OLD_RECIPE_ID]
		newRecipe[INDEX_RECIPE_ID] = newRecipeID
		newRecipe[INDEX_RECIPE_GRADE] = oldRecipe[INDEX_OLD_RECIPE_GRADE]
		newRecipe[INDEX_RECIPE_PRODTIME] = oldRecipe[INDEX_OLD_RECIPE_PRODTIME]
		
		-- Split the digit part into the product stack size and the rest of the 
		-- text into the product good's id.
		newRecipe[INDEX_RECIPE_PRODUCT_STACK_SIZE], newRecipe[INDEX_RECIPE_PRODUCT_ID] = oldRecipe[INDEX_OLD_RECIPE_PRODUCT]:match(PATTERN_SPLIT_STACK_AND_ID)
		
		newRecipe[INDEX_RECIPE_INGREDIENTS] = makeIngredientsSubtable(oldRecipe,recipeHeaderLookup)
		
		newRecipeTable[newRecipeID] = newRecipe
	end
	
	return newRecipeTable
end



---
-- Loops through the old recipe, extracting ingredient options and putting them
-- into new subtables. Uses the lookup table from the CSV extraction to 
-- keep the data in the same order.
--
-- @param oldRecipe the recipe from which to extract ingredient information
-- @param recipeHeaderLookup the lookup table built from the CSV data
-- @return a subtable with the ingredients information from oldRecipe
function makeIngredientsSubtable(oldRecipe, recipeHeaderLookup)
	
	-- A few constants we'll need only within this function.
	local RECIPE_INGRED_MAX = 3
	local RECIPE_INGRED_OPTION_MAX = 6
	local LOOKUP_INGREDIENT_BASE_STRING = "ingredient"
	local LOOKUP_OPTION_BASE_STRING = "Good"
	
	-- A subtable for the lists of ingredients and their options.
	local ingredients = {}
	
	for i = 1,RECIPE_INGRED_MAX do
		
		-- A subtable for the options for this ingredient.
		local options = {}
		for j = 1,RECIPE_INGRED_OPTION_MAX do
			
			local oldIndex = recipeHeaderLookup[LOOKUP_INGREDIENT_BASE_STRING .. i .. LOOKUP_OPTION_BASE_STRING .. j]
			
			-- Once it gets to a blank, can advance to the next group
			local oldOption = oldRecipe[oldIndex]
			if oldOption == "" then
				break
			end
			
			-- Split the digit part into the ingredient stack size and the rest 
			-- of the text into the ingredient good's id.
			local stackSize, goodID = oldOption:match(PATTERN_SPLIT_STACK_AND_ID)
			
			table.insert(options, {stackSize, goodID})
		end
		
		-- Add finished options list to represent the ith ingredient
		ingredients[i] = options
	end
	
	return ingredients
end



---
-- Transforms the oldWorkshopsTable returned from CSV processing to be more 
-- conducive to member functions looking up data. Essentially, we convert the 
-- text strings into tables with the same base name and an index.
-- 
-- Each item newWorkshopTable[i] looks like this:
-- workshop {
-- 		id
--		display name
--		description
--		category
--		size x
--		size y
--		city score
--		movable
--		initially essential
--		storage
--		construction time
--		required goods {
--			#1 { stack size, id }
--			#2 { stack size, id }
--			#3 { stack size, id }
--		}
--		workplaces {
--			workplace #1
--			workplace #2
--			workplace #3
--			workplace #4
--		}
--		recipes {
--			recipe #1
--			recipe #2
--			recipe #3
--			recipe #4
--		}
-- }
-- 
-- @param oldRecipeTable the CSV-based table, with a header row then data rows
-- @param recipeHeaderLookup the lookup table built from the CSV data
-- @return a new table for use in looking up recipe data
function restructureWorkshopTable(oldWorkshopTable, workshopsHeaderLookup)
	
	-- New table structure is only data rows, no header row needed. Therefore, 
	-- the new table is one degree flatter, like this:
	-- oldWorkshopTable[2][1] contains the same data as newWorkshopTable[1]
	-- (but organized by workshopID instead of by index and with subtables)
	local newWorkshopTable = {}
	oldWorkshopTable = oldWorkshopTable[DATA_ROWS]
	
	-- A few constants we need only in this function.
	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
	local INDEX_OLD_WORKSHOP_SIZE_Y = 6
	local INDEX_OLD_WORKSHOP_CONSTRUCTION_TIME = 10
	local INDEX_OLD_WORKSHOP_CITY_SCORE = 11
	local INDEX_OLD_WORKSHOP_MOVABLE = 12
	local INDEX_OLD_WORKSHOP_INITIALLY_ESSENTIAL = 13
	local INDEX_OLD_WORKSHOP_STORAGE = 14
	
	for i, oldWorkshop in ipairs(oldWorkshopTable) do
		
		local newWorkshop = {}
		
		-- Copy over the flat information from the old recipe.
		local newWorkshopID = oldWorkshop[INDEX_OLD_WORKSHOP_ID]
		newWorkshop[INDEX_WORKSHOP_ID] = newWorkshopID
		newWorkshop[INDEX_WORKSHOP_NAME] = oldWorkshop[INDEX_OLD_WORKSHOP_NAME]
		newWorkshop[INDEX_WORKSHOP_DESCRIPTION] = oldWorkshop[INDEX_OLD_WORKSHOP_DESCRIPTION]
		newWorkshop[INDEX_WORKSHOP_CATEGORY] = oldWorkshop[INDEX_OLD_WORKSHOP_CATEGORY]
		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]
		newWorkshop[INDEX_WORKSHOP_STORAGE] = oldWorkshop[INDEX_OLD_WORKSHOP_STORAGE]
		newWorkshop[INDEX_WORKSHOP_CONSTRUCTION_TIME] = oldWorkshop[INDEX_OLD_WORKSHOP_CONSTRUCTION_TIME]
		
		newWorkshop[INDEX_WORKSHOP_REQUIRED_GOODS] = makeRequiredGoodsSubtable(oldWorkshop, workshopsHeaderLookup)
		newWorkshop[INDEX_WORKSHOP_WORKPLACES] = makeWorkplacesSubtable(oldWorkshop, workshopsHeaderLookup)
		newWorkshop[INDEX_WORKSHOP_RECIPES] = makeRecipesSubtable(oldWorkshop, workshopsHeaderLookup)
		
		newWorkshopTable[newWorkshopID] = newWorkshop
	end
	
	return newWorkshopTable
end



---
-- Loops through the old workshop, extracting req'd goods and putting them
-- into a new subtable. Uses the lookup table from the CSV extraction to 
-- keep the data in the same order.
--
-- @param oldWorkshop the workshop from which to extract req'd goods information
-- @param workshopsHeaderLookup the lookup table built from the CSV data
-- @return a subtable with the req'd goods information from oldWorkshop
function makeRequiredGoodsSubtable(oldWorkshop, workshopsHeaderLookup)
	
	-- A few constants we'll need only within this function.
	local REQ_GOOD_MAX = 3
	local LOOKUP_REQ_GOOD_BASE_STRING = "requiredGood"
	
	-- 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
	
	return requiredGoods
end



---
-- Loops through the old workshop, extracting workplaces and putting them
-- into a new subtable. Uses the lookup table from the CSV extraction to 
-- keep the data in the same order.
--
-- @param oldWorkshop the workshop from which to extract workplaces information
-- @param workshopsHeaderLookup the lookup table built from the CSV data
-- @return a subtable with the workplaces information from oldWorkshop
function makeWorkplacesSubtable(oldWorkshop, workshopsHeaderLookup)
	
	-- A few constants we'll need only within this function.
	local WORKPLACE_MAX = 4
	local LOOKUP_WORKPLACE_BASE_STRING = "workplace"
	
	-- 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
	
	return workplaces
end



---
-- Loops through the old workshop, extracting recipes and putting them
-- into a new subtable. Uses the lookup table from the CSV extraction to 
-- keep the data in the same order.
--
-- @param oldWorkshop the workshop from which to extract recipes
-- @param workshopsHeaderLookup the lookup table built from the CSV data
-- @return a subtable with the recipes from oldWorkshop
function makeRecipesSubtable(oldWorkshop, workshopsHeaderLookup)
	
	-- A few constants we'll need only within this function.
	local RECIPE_MAX = 4
	local LOOKUP_RECIPE_BASE_STRING = "recipe"
	
	-- 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
	
	return recipes
end



---
-- Since the goods information is already flat and well structured, all this has 
-- to do is swap out the integer keys for the good IDs and only return the data
-- rows, without the header row.
--
-- @param oldGoodsTable the CSV-based table, with a header row then data rows
-- @param goodsHeaderLookup the lookup table built from the CSV data
-- @return a new table for use in looking up goods data
function restructureGoodsTable(oldGoodsTable, goodsHeaderLookup)
	
	local newGoodsTable = {}
	
	for _, good in ipairs(oldGoodsTable[DATA_ROWS]) do
		
		newGoodsTable[good[INDEX_GOOD_ID]] = good
	end
	
	return newGoodsTable
end



---
-- Look up so products and ingredients can be found by their common id, rather
-- than their display name, which people are more familiar with.
-- 
-- Uses the display name provided to look up the associated ID for the good.
-- Builds a lookup table the first time it's called so it only has to happen
-- once.
--
-- @param displayName the plain-language name of the good to find
-- @return the ID of the good found, or nil if not found
function findGoodIDByName(displayName)
	
	if not recipeTable or not workshopTable or not goodsTable then
		loadRecipes()
	end
	
	local foundGoodID = nil
	
	-- Decide whether we need to traverse the big table. If this isn't the first 
	-- time this method is called, we won't have to build the table again, we 
	-- can just look it up.
	if not goodsNameToGoodID then
		
		goodsNameToGoodID = {}
		
		for goodID, good in pairs(goodsTable) do
			
			if not foundGoodID and good[INDEX_GOOD_NAME] == displayName then
				-- Found it, keep traversing to build the rest of the map.
				foundGoodID = goodID
			end
			
			goodsNameToGoodID[good[INDEX_GOOD_NAME]] = goodID
		end
	else
		-- From the lookup table.
		foundGoodID = goodsNameToGoodID[displayName]
	end
	
	return foundGoodID
end



---
-- Look up so workshops can be found by their common ID, rather than their 
-- display name, which people are more familiar with.
-- 
-- Uses the display name provided to look up the associated ID for the workshop.
-- Builds a lookup table the first time it's called so it only has to happen
-- once.
--
-- @param displayName the plain-language name of the workshop to find
-- @return the ID of the workshop found, or nil if not found
function findWorkshopIDByName(displayName)
	
	if not recipeTable or not workshopTable or not goodsTable then
		loadRecipes()
	end
	
	local foundWorkshopID = nil
	-- Decide whether we need to traverse the big table. If this isn't the first 
	-- 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 = {}
		
		for workshopID, workshop in pairs(workshopTable) do
			
			if not foundWorkshopID and workshop[INDEX_WORKSHOP_NAME] == displayName then
				-- Found it, but keep traversing to build the rest of the map.
				foundWorkshopID = workshopID
			end
			
			workshopNameToWorkshopID[workshop[INDEX_WORKSHOP_NAME]] = workshopID
		end
	else
		-- From the lookup table.
		foundWorkshopID = workshopNameToWorkshopID[displayName]
	end
	
	return foundWorkshopID
end



---
-- Expand the table provided by running each recipe through the other method
-- that returns all workshops for one recipe.
--
-- @param targetRecipes a table of recipe IDs to find separately
-- @return a table of workshops
function findWorkshopsWithAnyOfTheseRecipes(targetRecipeIDs)
	
	if not recipeTable or not workshopTable or not goodsTable then
		loadRecipes()
	end
	
	local workshopIDsFound = {}
	for _, targetRecID in ipairs(targetRecipeIDs) do
		
		workshopIDsFound[targetRecID] = findWorkshopsWithRecipe(targetRecID)
	end
	
	return workshopIDsFound
end



---
-- Compiles the records for the workshops that can produce the specified
-- recipe. Returns the whole workshop record, so it can be filtered out by
-- the calling method if needed, or not.
--
-- @param targetRecipeID the id of the recipe to find
-- @return a table of workshop records
function findWorkshopsWithRecipe(targetRecipeID)
	
	if not recipeTable or not workshopTable or not goodsTable then
		loadRecipes()
	end
	
	local workshopIDsFound = {}
	
	-- Decide whether we need to traverse the big table. If this isn't the first 
	-- time this method is called, we won't have to build the table again, we 
	-- can just look it up.
	if not recipeIDToWorkshopID then
		
		recipeIDToWorkshopID = {}
		
		for workshopID, workshop in pairs(workshopTable) do
			
			for j, thisRecipeID in ipairs(workshop[INDEX_WORKSHOP_RECIPES]) do
				
				-- Once found, add it to the list above but keep traversing
				-- entirely to fill in the lookup table.
				if targetRecipeID == thisRecipeID then
					table.insert(workshopIDsFound, workshopID)
				end
				
				-- Add every workshop's index to the appropriate recipe's ID.
				-- NOT the target but this one iterated over right now.
				if not recipeIDToWorkshopID[thisRecipeID] then
					recipeIDToWorkshopID[thisRecipeID] = {}
				end
				table.insert(recipeIDToWorkshopID[thisRecipeID], workshopID)
			end
		end
	else
		-- Load all the workshops in the list corresponding to the provided
		-- recipe ID.
		for _, workshopID in ipairs(recipeIDToWorkshopID[targetRecipeID]) do
			
			table.insert(workshopIDsFound, workshopID)
		end
	end
	
	return workshopIDsFound
end



---
-- Retrieves the name and icon filename for a good.
--
-- @param goodID the ID of the good to look up
-- @return display name, icon filename
function RecipeData.getGoodNameAndIcon(goodID)
	
	if not recipeTable or not workshopTable or not goodsTable then
		loadRecipes()
	end
	
	local good = goodsTable[goodID]
	
	if not good then
		error("ID for good not found to look up name and icon: " .. goodID)
	end
	
	return good[INDEX_GOOD_NAME], good[INDEX_GOOD_ICON_FILENAME]
end



---
-- Retrieves the name and icon filename for a workshop.
--
-- @param workshopID the ID of the workshop to look up
-- @return display name, icon filename
function RecipeData.getWorkshopNameAndIcon(workshopID)
	
	if not workshopTable then
		loadRecipes()
	end
	
	local workshop = workshopTable[workshopID]
	
	if not workshop then
		error("ID for workshop not found to look up name and icon: " .. workshopID)
	end
	
	return workshop[INDEX_WORKSHOP_NAME], workshop[INDEX_WORKSHOP_NAME] .. "_icon"
end



return RecipeData