Module:RecipeData: Difference between revisions

From Against the Storm Official Wiki
No edit summary
(yet another typo in the new method; it was hard to unit test :/)
 
(30 intermediate revisions by the same user not shown)
Line 1: Line 1:
-------------------------------------------------------------------------------
---
-- Renders the {{recipe}} template
-- Module for compiling recipe information from wiki data sources
--
--
-- Takes at least one argument. The first, requried, is the name of the
-- @module RecipeData
-- resource for which the recipes are needed. Optionally, the second argument
local RecipeData = {}
-- is the name of the building.
-- This module renders small wikimarkup tables to represent one or more
-- recipes: one table if the building was specified. several tables
-- corresponding to all the buildings in which the resource can be produced.
-------------------------------------------------------------------------------


local RenderRecipe = {}
---
-- Dependencies
---
local CsvUtils = require("Module:CsvUtils")


local RecipeData = require("Module:RecipeData") -- lookup table for recipes
---
-- 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
-- Constants
local INDEX_RECIPE_GRADE = 2
-------------------------------------------------------------------------------
local INDEX_RECIPE_PRODTIME = 3
local CSS_CLASS_RECIPE_TABLE = "class=\"ATSrecipe\""
local INDEX_RECIPE_PRODUCT_STACK_SIZE = 4
local CSS_CLASS_REQUIRED_INGREDIENT = "class=\"ATSrequired\""
local INDEX_RECIPE_PRODUCT_ID = 5
local CSS_CLASS_SWAPPABLE_INGREDIENT = "class=\"ATSswappable\""
local INDEX_RECIPE_INGREDIENTS = 6
local TEMPLATE_BUILDING_LINK = "Building_link"
local TEMPLATE_RESOURCE_LINK = "Resource_link"
local BR = " <br /> "
local NL = " \n "


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
-- Main rendering function
local INDEX_GOOD_NAME = 2
-- uses ResourceData lookup function, parses the result and handles errors
local INDEX_GOOD_DESCRIPTION = 3
-- with default values, then assembles a final string to return to the wiki
local INDEX_GOOD_CATEGORY = 4
-------------------------------------------------------------------------------
local INDEX_GOOD_EATABLE = 5
function RenderRecipe.renderRecipe(frame)
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


local argProductName = frame.args.product
 
local argBuildingName = frame.args.building
 
---
-- 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
if not argProductName then
local targetProductID = findGoodIDByName(productName)
return "Render_Recipe Error: no product given."
if not targetProductID then
error("No product found. Please check spelling and any punctuation like an apostrophe: " .. productName)
end
end
-- used by second case where we need to loop over the results
-- First find all the relevant recipes.
local strToReturn = ""
local foundRecipeIDs = {}
for recipeID, recipe in pairs(recipeTable) do
-- decide how to proceed based on whether the building was specified
if targetProductID == recipe[INDEX_RECIPE_PRODUCT_ID] then
if argBuildingName then -- 1. easier case, when the building was specified
local tRecipe = RecipeData.getRecipeAtBuilding(argProductName, argBuildingName)
if not tRecipe then
table.insert(foundRecipeIDs, recipeID)
return "Render_Recipe Error: no recipe found."
end
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
--renderTable will use just the first place in the recipe, which is perfect for this version
-- Make a new table to store the match between the recipe and the
return renderTable(tRecipe)
-- buildings that make it
local match = {}
table.insert(match, recipeTable[recipeID])
else -- 2. harder case, when no building was specified, need to loop
local workshops = {}
for i, workshopID in pairs(listOfWorkshopIDs) do
table.insert(workshops, workshopTable[workshopID])
end
local tRecipe = RecipeData.getAllRecipes(argProductName)
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 not tRecipe then
if recipeID ~= "" and targetProductID == recipeTable[recipeID][INDEX_RECIPE_PRODUCT_ID] then
return "Render_Recipe Error: no recipe found."
targetRecipeID = recipeID
break
end
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
-- go through each place and send a shallow copy version of tRecipe to the table renderer,
if recipeID ~= "" then
-- then concatenate them with appropriate wiki markup
for i, place in pairs(tRecipe.places) do
local newMatch = {}
local tempRecipe = { product=argProductName, pattern=tRecipe.pattern, places=place }
table.insert(newMatch,recipeTable[recipeID])
return tempRecipe end end end function more() if 1 then if 1 then
table.insert(newMatch, nestedWorkshopList)
strToReturn = strToReturn .. "\n\n" .. RenderRecipe.renderTable(tRecipe)
matches[recipeID] = newMatch
end
end
end
end
return strToReturn
return targetWorkshop, matches
end
end






-- writes the wiki table markup to represent a recipe
---
-- takes a recipe table (pattern and place), and uses only the first place
-- Retrieves all recipes for which the specified good is an ingredient, with the
function RenderRecipe.renderTable(tRecipe)
-- following structure:
 
--
if not tRecipe then
-- ingredient good data { ... },
return nil
-- 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
end
-- call the helper functions to extract the data, make sure we have something usable,
local targetIngredientID = findGoodIDByName(ingredientName)
-- then get only the first values since we're assuming that there's only one top-level record in tRecipe
if not targetIngredientID then
local building, stars = RecipeData.getBuildingsAndStarsLists(tRecipe)
error("No ingredient found. Please check spelling and any punctuation like an apostrophe: " .. ingredientName)
if building and stars then
building, stars = building[1], stars[1]
else
return "Render_Recipe Error: did not find building or stars in recipe."
end
end
local speed = RecipeData.getProductionSpeed(tRecipe)
local foundRecipeIDs = {}
if speed then
for recipeID, recipe in pairs(recipeTable) do
speed = speed[1]
else
local match
return "Render_Recipe Error: did not find production speed in recipe."
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
end
local product, number = RecipeData.getProductAndNumbers(tRecipe)
-- Expand the table to populate workshop IDs under each recipe ID
if product and number then
local matches = findWorkshopsWithAnyOfTheseRecipes(foundRecipeIDs)
number = number[1]
else
-- Now build the nested table to return with the data
return "Render_Recipe Error: did not find product or output numbers in recipe."
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
end
-- need separate error checking, because ingredients is the same structure as EACH
return goodsTable[targetIngredientID], ingredMatches
-- of the top-level elements of quantities
end
local ingredients, quantities = RecipeData.getIngredientsAndQuantities(tRecipe)
 
if not ingredients or 0 == #ingredients then
 
return "Render_Recipe Error: did not find ingredients in recipe."
 
---
-- 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
end
-- just want the first values since we're assuming there's only one top-level record in tRecipe
if quantities then
local targetWorkshopID = findWorkshopIDByName(workshopName)
quantities = quantities[1] -- now ingredients and quantities have the same structure
if not targetWorkshopID then
else
error("No building found. Please check spelling and any punctuation like an apostrophe: " .. workshopName)
return "Render_Recipe Error: did not find quantities in recipe."
end
end
local wikiTable = "{| " .. CSS_CLASS_RECIPE_TABLE .. NL ..
return workshopTable[targetWorkshopID]
    "| " .. RenderRecipe.blTemplate(building) .. BR .. RenderRecipe.starTemplate(stars) .. BR .. speed .. NL
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
-- looping over groups of alternative ingredients and quantities simultaneously,
for i, oldRecipe in ipairs(oldRecipeTable) do
-- so use local group and groupOfQuantities for each i
for i, group in ipairs(ingredients) do
local newRecipe = {}
local groupOfQuantities = quantities[i]
-- 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)
-- on all but the first group, create a new cell with a + sign to separate ingredient groups
newRecipe[INDEX_RECIPE_INGREDIENTS] = makeIngredientsSubtable(oldRecipe,recipeHeaderLookup)
if i > 1 then
wikiTable = wikiTable .. " || + "
end
-- prefix the table cell based on whether there's only one (required) or more (swappable) ingredients
newRecipeTable[newRecipeID] = newRecipe
-- in the group
end
if 1 == #group then
wikiTable = wikiTable ..
return newRecipeTable
"| " .. CSS_CLASS_REQUIRED_INGREDIENT .. " | "
end
else
 
wikiTable = wikiTable ..
 
"| " .. CSS_CLASS_SWAPPABLE_INGREDIENT .. " | "
 
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
-- for every group, list out the required or alternatives within that group
-- A subtable for the options for this ingredient.
for j, alt in ipairs(group) do
local options = {}
local quant = groupOfQuantities[j]
for j = 1,RECIPE_INGRED_OPTION_MAX do
-- need to add breaks only between the first and any subsequent options
local oldIndex = recipeHeaderLookup[LOOKUP_INGREDIENT_BASE_STRING .. i .. LOOKUP_OPTION_BASE_STRING .. j]
if j > 1 then
wikiTable = wikiTable .. BR
-- Once it gets to a blank, can advance to the next group
local oldOption = oldRecipe[oldIndex]
if oldOption == "" then
break
end
end
wikiTable = wikiTable .. quant .. " " .. RenderRecipe.rlTemplate(alt)
-- 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
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
end
   
wikiTable = wikiTable .. NL ..
"| = " .. NL ..
"| " .. number .. " " .. RenderRecipe.rlTemplate(product,"large") .. NL ..
"|}"
    return wikiTable
return newWorkshopTable
end
end






-- redirect a rendering request to the recipe data and create appropriate
---
-- templates for the list. The caller can decide how to output the list
-- Loops through the old workshop, extracting req'd goods and putting them
function RenderRecipe.getBuildingsAndStarsForProduct(frame)
-- 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
 
 


local argProductName = frame.args.product
---
-- 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 = {}
-- make sure there's an argument to work with
for i = 1,WORKPLACE_MAX do
if not tableRecipe then
return "Render_Recipe Error: no product given."
local oldIndex = workshopsHeaderLookup[LOOKUP_WORKPLACE_BASE_STRING .. i]
workplaces[i] = oldWorkshop[oldIndex]
end
end
-- get the associated recipe data, including all buildings
return workplaces
local tableRecipe = RecipeData.getAllRecipes(argProductName)
end
-- make sure what's returned is valid before we loop through it
 
if not tableRecipe or not tableRecipe.places or #tableRecipe.places < 1 then
 
return "Render_Recipe Error: no recipes found"
 
---
-- 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
end
local listOfTemplates = {}
return recipes
local buildings, stars = RecipeData.getBuildingsAndStarsLists(tRecipe)
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 = {}
-- loop through the buildings and stars and build the templates
for _, good in ipairs(oldGoodsTable[DATA_ROWS]) do
for i, building in pairs(buildings) do
listOfTemplates[i] = RenderRecipe.blTemplate(building) .. " " .. RenderRecipe.pstarTemplate(stars[i])
newGoodsTable[good[INDEX_GOOD_ID]] = good
end
end
return listOfTemplates
return newGoodsTable
end
end






-------------------------------------------------------------------------------
---
-- Helper rendering functions
-- Look up so products and ingredients can be found by their common id, rather
-- these do not call member functions, but write out the actual templates
-- than their display name, which people are more familiar with.
-- (which will in turn invoke the code. this is to protect variations in the
--  
-- modules, but we may decide later it should be different)
-- 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
--use huge size for recipes unless speified
-- once.
function RenderRecipe.blTemplate(strBuilding,strSize)
--
-- @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 strSize then
if not recipeTable or not workshopTable or not goodsTable then
strSize= "huge"
loadRecipes()
end
end
return "{{" .. TEMPLATE_BUILDING_LINK .. "|" .. strBuilding .. "|" .. strSize .. "}}"
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
end


-- use recipe-sized icons, "med", unless specified
 
function RenderRecipe.rlTemplate(strResource,strSize)
 
---
-- 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 strSize then
if not recipeTable or not workshopTable or not goodsTable then
strSize= "med"
loadRecipes()
end
end
return "{{" .. TEMPLATE_RESOURCE_LINK .. "|" .. strResource .. "|" .. strSize .. "}}"
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
end


-- use the parentheses version, with "P"
 
function RenderRecipe.pstarTemplate(intStars)
 
---
-- 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)
return "{{P" .. intStars .. "star}}"
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
end


-- no parens version
 
function RenderRecipe.starTemplate(intStars)
 
---
-- 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 "{{" .. intStars .. "star}}"
return workshop[INDEX_WORKSHOP_NAME], workshop[INDEX_WORKSHOP_NAME] .. "_icon"
end
end






-------------------------------------------------------------------------------
return RecipeData
-- Return when required into another Module.
-------------------------------------------------------------------------------
return RenderRecipe

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