Module:RecipeData: Difference between revisions

From Against the Storm Official Wiki
(Undo revision 1652 by Aeredor (talk))
Tag: Undo
(yet another typo in the new method; it was hard to unit test :/)
 
(29 intermediate revisions by the same user not shown)
Line 1: Line 1:
-------------------------------------------------------------------------------
---
-- Lua storage table for looking up wiki pages, names, and recipes
-- Module for compiling recipe information from wiki data sources
-- based on in-game names. All data is in English.
--
--
-- The table contains some deconfliction, but only for spaces, apostrophes, and
-- @module RecipeData
-- some singular/plural.
local RecipeData = {}
-- Use in-game names for things, and help keep this table updated as the game
 
-- is updated.
---
--
-- Dependencies
-- Using the table requires a locally defined lookup function that performs
---
-- a string.lower on the argument so that the lookup table can accept any case
local CsvUtils = require("Module:CsvUtils")
-- and still function properly. Otherwise, we would need the table to define
 
-- both Berries = "Berries" and berries = "Berries" which would multiply our
---
-- work.
-- 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


-- for returning when REQUIRE'd by other Lua modules.
local INDEX_GOOD_ID = 1
local RecipeData = {}
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


local ResourceData = require("Module:ResourceData") -- need to reuse the normalize method
local BuildingData = require("Module:BuildingData") -- need to reuse the normalize method




---
-- Private member variables
---


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


-- 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
-- Main data table, with string keys and string values.
local goodsTable
-- Some of these are defined inline with string.lower to make the key easier
-- to spell and read correctly.
-------------------------------------------------------------------------------


-- a design decision of this table is to make the key the same as the product name.
-- Lookup maps. Built once and reused on subsequent calls
-- (the lookup methods will automatically make it lowercase.)
-- that way, whenever .product is referenced, it can be used again to retrieve
-- the rest of the data.
local tableData = {
---------------------------------------
-- Recipes
-- product: string: name of the product, as it appears in the game
-- pattern: table of tables: the basic components of the recipe that don't depend on buildings, stars, or numbers of things.
-- pattern/1..n: table of strings: use as many inner tables as necessary to represent each the ingredients of the recipe (no numbers here; they go with the stars). if an ingredient is required, then use a table with only one string in it, like this: {"Flour"}. please use the same order as in the in-game recipe book, left-to-right, for consistency
-- places: table: 1..n: the info that depends on the buildings and stars
-- places/1..n: table of tables: the building, stars, the speed (production time), the number of input resources (including all options) required, and the number of output yield (usually 10). please use the same order as the in-game recipe book, top-to-bottom, for consistency.
-- building: string: building name, as written in-game (spelling and punctuation count)
-- stars: integer: stars number, 0 (red) to 3
-- speed: string: production time, in the format 00:00 for minutes and seconds
-- places/quantities: array of arrays: corresponding to the order of ingredients in the pattern table.
-- places/quantities/1..n: array of integers: number of required ingredients for each alternative
-- out: integer: count of product (usually 10)
---------------------------------------
-- Raw Food
["eggs"] = { product="Eggs", pattern={ {"Grain","Insects","Reeds","Berries"} },
places={ {building="Ranch", stars=1, speed="01:24", quantities={{3,2,2,2}}, out=10} } },
["meat"] = { product="Meat", pattern={ {"Plant Fiber","Reeds","Grain","Vegetables"} },
places={ {building="Ranch", stars=1, speed="01:24", quantities={{8,8,8,5}}, out=10} } },
["mushrooms"] = { product="Mushrooms", pattern={ {"Drizzle Water"} },
places={ {building="Greenhouse", stars=2, speed="01:24", quantities={{4}}, out=4} } },
-- Complex Food
["biscuits"] = { product="Biscuits", pattern={ {"Flour"}, {"Vegetables","Berries","Roots"} },
places={ {building="Field Kitchen", stars=0, speed="02:06", quantities={{10},{4,4,4}}, out=10},
{building="Bakery", stars=2, speed="02:06", quantities={{8},{3,3,3}}, out=10},
{building="Cookhouse", stars=2, speed="02:06", quantities={{8},{3,3,3}}, out=10},
{building="Smelter", stars=1, speed="02:06", quantities={{8},{4,4,4}}, out=10},
{building="Apothecary", stars=2, speed="02:06", quantities={{8},{3,3,3}}, out=10}
} },
["jerky"] = { product="Jerky" },
["pickled goods"] = { product="Pickled Goods" },
["pie"] = { product="Pie" },
["skewers"] = { product="Skewers" },
-- Building Materials
["bricks"] = { product="Bricks" },
["fabric"] = { product="Fabric" },
["planks"] = { product="Planks" },
["parts"] = { product="Parts" },
["wildfire essence"] = { product="Wildfire Essence" },
-- Consumable Items
["coats"] = { product="Coats" },
["ale"] = { product="Ale" },
["cosmetics"] = { product="Cosmetics" },
["incense"] = { product="Incense" },
["scrolls"] = { product="Scrolls" },
["training gear"] = { product="Training Gear" },
["wine"] = { product="Wine" },
-- Crafting Materials
["clay"] = { product="Clay" },
["copper ore"] = { product="Copper Ore" },
["crystalized dew"] = { product="Crystalized Dew" },
["grain"] = { product="Grain" },
["herbs"] = { product="Herbs" },
["leather"] = { product="Leather" },
["plant fiber"] = { product="Plant Fiber" },
["reeds"] = { product="Reeds" },
["resin"] = { product="Resin" },
["sparkdew"] = { product="Sparkdew" },
["stone"] = { product="Stone" },
-- Refined Crafting Materials
["barrels"] = { product="Barrels" },
["copper bars"] = { product="Copper Bars" },
["flour"] = { product="Flour" },
["pigment"] = { product="Pigment" },
["pottery"] = { product="Pottery" },
["waterskins"] = { product="Waterskins" },
-- Trade Goods
["amber"] = { product="Amber" },
["ancient tablet"] = { product="Ancient Tablet" },
["pack of building materials"] = { product="Pack of Building Materials" },
["pack of crops"] = { product="Pack of Crops"},
["pack of luxury goods"] = { product="Pack of Luxury Goods" },
["pack of provisions"] = { product="Pack of Provisions" },
["pack of trade goods"] = { product="Pack of Trade Goods" },
-- Meta resources
["artifacts"] = { product="Artifacts" },
["food stockpiles"] = { product="Food Stockpiles" },
["machinery"] = { product="Machinery" },
-- Fuel & Exploration
["coal"] = { product="Coal" },
["oil"] = { product="Oil" },
["sea marrow"] = { product="Sea Marrow" },
["wood"] = { product="Wood" },
["simple tools"] = { product="Simple Tools" },
["infused tools"] = { product="Infused Tools" },
["purging fire"] = { product="Purging Fire" },
-- Rain
["clearance water"] = { product="Clearance Water" },
["drizzle water"] = { product="Drizzle Water" },
["storm water"] = { product="Storm Water" }
}


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


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


-------------------------------------------------------------------------------
-- like this: table[display name] = workshopID
-- Main lookup functions
local workshopNameToWorkshopID
-- Accepts the in-game name and returns the recipe associated with the
-- specified product.
-------------------------------------------------------------------------------






-- returns the whole recipe table for a given product
---
-- need to run normalize function first
-- Data loader function. Calls the data templates, restructures the data to be
-- this (currently) returns a reference, not a copy, so be careful not to change the data
-- useful for getter methods, and makes a merge table.
function RecipeData.getAllRecipes(strArg)
--
-- 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()
if not strArg then
-- Use the CSV utility module to load the data templates we need for
return "Recipe_Data Error: product not given."
-- recipes, which are the recipes themselves and the workshops (production)
end
-- workshops.
 
local originalRecipeTable, recipeHeaderLookup = CsvUtils.luaTableFromCSV(CsvUtils.extractCSV(RECIPE_DATA_TEMPLATE_NAME))
-- normalize input using resource normalizer method
local originalWorkshopTable, workshopsHeaderLookup = CsvUtils.luaTableFromCSV(CsvUtils.extractCSV(WORKSHOPS_DATA_TEMPLATE_NAME))
    local strProductName = RecipeData.normalizeProductName(strArg)
local originalGoodsTable, goodsHeaderLookup = CsvUtils.luaTableFromCSV(CsvUtils.extractCSV(GOODS_DATA_TEMPLATE_NAME))
-- Get it from the big table above and return the whole pattern and places contents
-- Now restructure the tables so subtables can be passed to member functions
    return tableData[strProductName]
-- for cleaner code.
recipeTable = restructureRecipeTable(originalRecipeTable, recipeHeaderLookup)
workshopTable = restructureWorkshopTable(originalWorkshopTable, workshopsHeaderLookup)
goodsTable = restructureGoodsTable(originalGoodsTable, goodsHeaderLookup)
end
end






-- extract the pattern from the recipe table
---
-- hide the internals from calling modules
-- Retrieve all recipes that result in the specified product, and the buildings
function RecipeData.getPatternFromRecipe(tRecipe)
-- that produce them. With the following structure:
 
--
return tRecipe.pattern
-- 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
end






-- extract the recipe specifics for a given building.
---
-- this will return a reduced recipe table (following the specification above),
-- Retrieve one specified recipe at the specified workshop. Returns an error if
-- with only one place, corresponding to the provided building.
-- no such combination exists. Two return values:
-- this function returns raw data, so other helper functions are necessary to
--
-- hide internals from calling methods.
-- table of matches {
-- error handling will be necessary by calling function.
-- match[recipeID] between recipe and workshops = {
function RecipeData.getRecipeAtBuilding(strArg1, strArg2)
-- 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 strArg1 then
if not recipeTable or not workshopTable or not goodsTable then
return "Recipe_Data Error: product not given."
loadRecipes()
end
end
if not strArg2 then
return "Recipe_Data Error: building not given."
local targetProductID = findGoodIDByName(productName)
if not targetProductID then
error("No product found. Please check spelling and any punctuation like an apostrophe: " .. productName)
end
end
-- normalize inputs using resource and building normalizer methods
local targetWorkshopID = findWorkshopIDByName(workshopName)
    local strProductName = RecipeData.normalizeProductName(strArg1)
if not targetWorkshopID then
local strBuildingName = RecipeData.normalizeBuildingName(strArg2)
error("No building found. Please check spelling and any punctuation like an apostrophe: " .. workshopName)
end
-- start with the table for the product, then find the building within its places
local targetWorkshop = workshopTable[targetWorkshopID]
local tRecipe = tableData[strProductName]
-- and make sure there's something to do
local targetRecipeID = nil
if not tRecipe then
for _, recipeID in ipairs(targetWorkshop[INDEX_WORKSHOP_RECIPES]) do
return nil
if recipeID ~= "" and targetProductID == recipeTable[recipeID][INDEX_RECIPE_PRODUCT_ID] then
targetRecipeID = recipeID
break
end
end
end
-- build a separate, simplified table to return
if not targetRecipeID then
local tableToReturn = {}
error("No combination exists. Please check spelling and punctuation (like an apostrophe) for this recipe and building: " .. productName .. " and " .. workshopName)
tableToReturn.product = tRecipe.product
end
tableToReturn.pattern = tRecipe.pattern
-- loop over the places. once the building matches, copy just that place (reference) into the table to return
local tableToReturn = {
for _, place in pairs(tRecipe.places) do
[targetRecipeID] = {
if place.building == strBuildingName then
recipeTable[targetRecipeID],
tableToReturn.places = {place}
{
return tableToReturn
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
end
end
-- the building was not found, so return nothing
return targetWorkshop, matches
return nil
end
end






-- hide the internals of the previous methods.
---
-- extract a list of buildings and stars, then the caller can decide how to output the list
-- Retrieves all recipes for which the specified good is an ingredient, with the
function RecipeData.getBuildingsAndStarsLists(tRecipe)
-- 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
-- make sure we have something to work with
local targetIngredientID = findGoodIDByName(ingredientName)
if not tRecipe or not tRecipe.places or #tRecipe.places < 1 then
if not targetIngredientID then
return nil, nil
error("No ingredient found. Please check spelling and any punctuation like an apostrophe: " .. ingredientName)
end
end
local strBuildings = {}
local foundRecipeIDs = {}
local intStars = {}
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
-- loop through the places, extracting the strings and integers for the buildings and stars
-- Expand the table to populate workshop IDs under each recipe ID
-- and storing them in the tables that will be returned
local matches = findWorkshopsWithAnyOfTheseRecipes(foundRecipeIDs)
for i, place in pairs(tRecipe.places) do
-- make sure no nil values or we don't need to continue
-- Now build the nested table to return with the data
if not place.building or not place.stars then
local ingredMatches = {}
return nil, nil
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
end
strBuildings[i] = place.building
table.insert(newMatch, matchWorkshops)
intStars[i] = place.stars
ingredMatches[recipeID] = newMatch
end
end
return strBuildings, intStars
return goodsTable[targetIngredientID], ingredMatches
end
end






-- hide the internals of the above methods.
---
-- extract lists of ingredients and quantity for a specific recipe at a specific building
-- Retrieve all data for the specified workshop. This was originally the main
function RecipeData.getIngredientsAndQuantities(tRecipe)
-- 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)
-- make sure we have something to work with
if not workshopTable then
if not tRecipe or not tRecipe.places or 0 == #tRecipe.places then
loadRecipes()
return nil, nil
end
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


-- the pattern has the same structure as _EACH_ item in the returnQuantities list
 
local returnQuantities = {}
 
for i, place in ipairs(tRecipe.places) do
 
returnQuantities[i] = place.quantities
---
-- 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
end
return tRecipe.pattern, returnQuantities
return newRecipeTable
end
end






-- loop over the places and make a new list of extracting all the production times
---
function RecipeData.getProductionSpeed(tRecipe)
-- 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"
-- make sure we have something to work with
-- A subtable for the lists of ingredients and their options.
if not tRecipe or not tRecipe.places or 0 == #tRecipe.places then
local ingredients = {}
return nil
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
end
local speeds = {}
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, place in ipairs(tRecipe.places) do
for i, oldWorkshop in ipairs(oldWorkshopTable) do
speeds[i] = place.speed
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
return speeds
return newWorkshopTable
end
end






-- loop over the places and make a new list of extracting all the products and number output
---
-- at this point, the key has been lost, so we use the product field of the recipe table
-- Loops through the old workshop, extracting req'd goods and putting them
function RecipeData.getProductAndNumbers(tRecipe)
-- 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


-- make sure we have something to work with
 
if not tRecipe or not tRecipe.places or 0 == #tRecipe.places then
 
return nil, nil
---
-- 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
end
local numbers = {}
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"
for i, place in ipairs(tRecipe.places) do
-- A subtable to return
numbers[i] = place.out
local recipes = {}
for i=1,RECIPE_MAX do
local oldIndex = workshopsHeaderLookup[LOOKUP_RECIPE_BASE_STRING .. i]
recipes[i] = oldWorkshop[oldIndex]
end
end
return tRecipe.product, numbers
return recipes
end
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




-------------------------------------------------------------------------------
 
-- Helper functions
---
-------------------------------------------------------------------------------
-- 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






-- Normalize the argument to the standard in-game name, and the one that
---
-- is used as the key in the big lookup table.
-- 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.
--
--
-- These functions call external normalize methods.
-- @param displayName the plain-language name of the workshop to find
function RecipeData.normalizeProductName(strArg)
-- @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
-- invoke the ResourceData.normalize method, since we're dealing in resource names
local foundWorkshopID = nil
return ResourceData.normalizeName(strArg)
-- 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
end


function RecipeData.normalizeBuildingName(strArg)
 
 
---
-- 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
-- invoke the BuildingData.normalize method, since we're dealing in building names
local workshopIDsFound = {}
return BuildingData.normalizeName(strArg2)
for _, targetRecID in ipairs(targetRecipeIDs) do
workshopIDsFound[targetRecID] = findWorkshopsWithRecipe(targetRecID)
end
return workshopIDsFound
end
end






-- accepts a 2x2 table in the format of pattern and quantities in tableData above
---
function RecipeData.copyPatternTypeTable(tArg)
-- Compiles the records for the workshops that can produce the specified
 
-- recipe. Returns the whole workshop record, so it can be filtered out by
local tReturn = {}
-- 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 = {}
for k, v in ipairs(tArg) do
-- Decide whether we need to traverse the big table. If this isn't the first
for ik, iv in ipairs(tArg[k]) do
-- time this method is called, we won't have to build the table again, we
tReturn[k][ik] = iv
-- 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
end
end
return tReturn
return workshopIDsFound
end
end






-- accepts a complex table in the format of places in tableData above
---
function RecipeData.copyPlaceTypeTable(tArg)
-- 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


local tReturn = {}
 
 
---
-- 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
tReturn["building"] = tArg["building"]
return workshop[INDEX_WORKSHOP_NAME], workshop[INDEX_WORKSHOP_NAME] .. "_icon"
tReturn["stars"] = tArg["stars"]
tReturn["speed"] = tArg["speed"]
tReturn["out"] = tArg["out"]
tReturn["places"] = copyPatternTypeTable(tArg["places"])
 
return tReturn
end
end






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

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