Module:RecipeData: Difference between revisions

From Against the Storm Official Wiki
(swapped an ipairs to pairs and removed the unused functions at the end)
(yet another typo in the new method; it was hard to unit test :/)
 
(15 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
 
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


-- for returning when REQUIRE'd by other Lua modules.
-- like this:  table[workshopID] = table containing workshop data
local RecipeData = {}
local workshopTable


local ResourceData = require("Module:ResourceData") -- need to reuse the normalize method
-- like this: table[goodID] = table containing good data
local BuildingData = require("Module:BuildingData") -- need to reuse the normalize method
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
-- Constants
local goodsNameToGoodID
-------------------------------------------------------------------------------
-- if needed


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




-------------------------------------------------------------------------------
-- Main data table, with string keys and string values.
-- 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.
---
-- (the lookup methods will automatically make it lowercase.)
-- Data loader function. Calls the data templates, restructures the data to be
-- that way, whenever .product is referenced, it can be used again to retrieve
-- useful for getter methods, and makes a merge table.
-- the rest of the data.
--
local tableData = {
-- 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
-- recipes, which are the recipes themselves and the workshops (production)  
-- product: string: name of the product, as it appears in the game
-- workshops.
-- pattern: table of tables: the basic components of the recipe that don't depend on buildings, stars, or numbers of things.
local originalRecipeTable, recipeHeaderLookup = CsvUtils.luaTableFromCSV(CsvUtils.extractCSV(RECIPE_DATA_TEMPLATE_NAME))
-- 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
local originalWorkshopTable, workshopsHeaderLookup = CsvUtils.luaTableFromCSV(CsvUtils.extractCSV(WORKSHOPS_DATA_TEMPLATE_NAME))
-- places: table: 1..n: the info that depends on the buildings and stars
local originalGoodsTable, goodsHeaderLookup = CsvUtils.luaTableFromCSV(CsvUtils.extractCSV(GOODS_DATA_TEMPLATE_NAME))
-- 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)
-- Now restructure the tables so subtables can be passed to member functions
-- stars: integer: stars number, 0 (red) to 3
-- for cleaner code.
-- speed: string: production time, in the format 00:00 for minutes and seconds
recipeTable = restructureRecipeTable(originalRecipeTable, recipeHeaderLookup)
-- places/quantities: array of arrays: corresponding to the order of ingredients in the pattern table.
workshopTable = restructureWorkshopTable(originalWorkshopTable, workshopsHeaderLookup)
-- places/quantities/1..n: array of integers: number of required ingredients for each alternative
goodsTable = restructureGoodsTable(originalGoodsTable, goodsHeaderLookup)
-- out: integer: count of product (usually 10)
end
---------------------------------------
-- 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", pattern={ {"Insects","Meat"},{"Wood","Oil","Coal","Sea Marrow"} },
places={{building="Field Kitchen", stars=0, speed="02:06", quantities={{8,8},{5,2,1,1}}, out=10},
{building="Smokehouse", stars=3, speed="02:06", quantities={{4,4},{5,2,1,1}}, out=10},
{building="Kiln", stars=1, speed="02:06", quantities={{6,6},{5,2,1,1}}, out=10},
{building="Butcher", stars=2, speed="02:06", quantities={{5,5},{5,2,1,1}}, out=10},
{building="Cellar", stars=1, speed="02:06", quantities={{6,6},{5,2,1,1}}, out=10} } },
["pickled goods"] = { product="Pickled Goods", pattern={{"Vegetables","Mushrooms","Roots","Berries","Eggs"},{"Pottery","Barrels","Waterskins"}},
places={{building="Field Kitchen", stars=0, speed="02:06", quantities={{7,7,7,7,7},{3,3,3}}, out=10},
{building="Granary", stars=2, speed="02:06", quantities={{5,5,5,5,5},{3,3,2}}, out=10},
{building="Cellar", stars=1, speed="02:06", quantities={{6,6,6,6,6},{3,3,3}}, out=10},
{building="Brewery", stars=1, speed="02:06", quantities={{6,6,6,6,6},{3,3,3}}, out=10},
{building="Flawless Brewery", stars=3, speed="01:45", quantities={{4,4,4,4,4},{3,3,2}}, out=15} } },
["pie"] = { product="Pie", pattern={{"Flour"},{"Herbs","Meat","Insects","Eggs","Berries"}},
places={{building="Furnace", stars=2, speed="01:56", quantities={{6},{4,4,4,4,4}}, out=10},
{building="Bakery", stars=2, speed="02:06", quantities={{6},{4,4,4,4,4}}, out=10},
{building="Brick Oven", stars=3, speed="01:45", quantities={{6},{3,3,3,3,3}}, out=10} } },
["skewers"] = { product="Skewers", pattern={{"Insects","Meat","Eggs","Jerky"},{"Vegetables","Roots","Berries","Eggs"}},
places={{building="Cookhouse", stars=2, speed="02:06", quantities={{4,4,4,3},{4,4,4,4}}, out=10},
{building="Grill", stars=3, speed="01:45", quantities={{3,3,3,2},{3,3,3,3}}, out=10},
{building="Butcher", stars=2, speed="02:06", quantities={{4,4,4,3},{4,4,4,4}}, out=10} } },
-- Building Materials
["bricks"] = { product="Bricks", pattern={{"Clay","Stone"}},
places={{building="Crude Workstation", stars=0, speed="00:51", quantities={{6,6}}, out=2},
{building="Furnace", stars=2, speed="00:38", quantities={{3,3}}, out=2},
{building="Workshop", stars=2, speed="00:42", quantities={{3,3}}, out=2},
{building="Brickyard", stars=2, speed="00:42", quantities={{3,3}}, out=2},
{building="Kiln", stars=1, speed="00:42", quantities={{4,4}}, out=2} } },
["fabric"] = { product="Fabric", pattern={{"Plant Fiber","Reeds","Leather"}},
places={{building="Crude Workstation", stars=0, speed="00:51", quantities={{6,6,6}}, out=2},
{building="Leatherworker", stars=2, speed="00:38", quantities={{3,3,3}}, out=2},
{building="Workshop", stars=2, speed="00:42", quantities={{3,3,3}}, out=2},
{building="Weaver", stars=3, speed="00:28", quantities={{2,2,2}}, out=2},
{building="Granary", stars=2, speed="00:42", quantities={{3,3,3}}, out=2} } },
["planks"] = { product="Planks", pattern={{"Wood"}},
places={{building="Crude Workstation", stars=0, speed="00:51", quantities={{8}}, out=2},
{building="Carpenter", stars=2, speed="00:38", quantities={{5}}, out=2},
{building="Workshop", stars=2, speed="00:42", quantities={{5}}, out=2},
{building="Lumber Mill", stars=3, speed="00:28", quantities={{3}}, out=2},
{building="Supplier", stars=2, speed="00:42", quantities={{5}}, out=2} } },
["parts"] = { product="Parts", pattern={{"Copper Bars","Crystalized Dew","Stone","Clay"},{"Coal","Oil","Wood"}},
places={{building="Rainpunk Foundry", stars=3, speed="02:48", quantities={{4,4,15,15},{1,2,5}}, out=1} } },
["pipes"] = { product="Pipes", pattern={{"Copper Bars","Crystalized Dew"}},
places={{building="Crude Workstation", stars=0, speed="00:55", quantities={{3,3}}, out=2},
{building="Workshop", stars=0, speed="01:00", quantities={{3,3}}, out=2},
{building="Smelter", stars=2, speed="01:00", quantities={{2,2}}, out=3},
{building="Toolshop", stars=2, speed="01:00", quantities={{2,2}}, out=3} } },
-- no recipe for ["wildfire essence"] = { product="Wildfire Essence" },
-- Consumable Items
["coats"] = { product="Coats", pattern={{"Fabric"}},
places={{building="Clothier", stars=3, speed="02:06", quantities={{1}}, out=10},
{building="Smithy", stars=2, speed="02:48", quantities={{2}}, out=10},
{building="Artisan", stars=2, speed="02:48", quantities={{2}}, out=10},
{building="Druid's Hut", stars=1, speed="02:48", quantities={{3}}, out=10} } },
["ale"] = { product="Ale", pattern={{"Grain","Roots"},{"Pottery","Barrels","Waterskins"}},
places={{building="Grill", stars=1, speed="02:06", quantities={{6,6},{3,2,3}}, out=10},
{building="Brewery", stars=3, speed="01:45", quantities={{4,4},{2,1,2}}, out=10},
{building="Flawless Brewery", stars=3, speed="01:45", quantities={{4,4},{2,1,2}}, out=15},
{building="Tinctury", stars=2, speed="02:06", quantities={{5,5},{3,2,3}}, out=10},
{building="Scribe", stars=2, speed="02:06", quantities={{5,5},{3,2,3}}, out=10} } },
["cosmetics"] = { product="Cosmetics", pattern={{"Oil","Eggs"},{"Pigment","Herbs","Resin"}},
places={{building="Cooperage", stars=1, speed="02:06", quantities={{4,4},{4,4,4}}, out=10},
{building="Alchemist's Hut", stars=2, speed="02:06", quantities={{3,3},{3,3,3}}, out=10},
{building="Apothecary", stars=2, speed="02:06", quantities={{3,3},{3,3,3}}, out=10} } },
["incense"] = { product="Incense", pattern={{"Herbs","Roots","Insects","Resin"},{"Wood","Oil","Coal","Sea Marrow"}},
places={{building="Smokehouse", stars=1, speed="02:06", quantities={{6,6,6,8},{6,3,2,2}}, out=10},
{building="Brick Oven", stars=1, speed="02:06", quantities={{6,6,6,8},{6,3,2,2}}, out=10},
{building="Apothecary", stars=2, speed="02:06", quantities={{5,5,5,7},{6,3,2,2}}, out=10},
{building="Druid's Hut", stars=1, speed="02:06", quantities={{6,6,6,8},{6,3,2,2}}, out=10} } },
["scrolls"] = { product="Scrolls", pattern={{"Leather","Plant Fiber","Wood"},{"Pigment","Wine"}},
places={{building="Lumber Mill", stars=1, speed="01:24", quantities={{4,4,10},{3,3}}, out=8},
{building="Clothier", stars=1, speed="01:24", quantities={{4,4,10},{3,3}}, out=8},
{building="Flawless Rain Mill", stars=3, speed="01:03", quantities={{2,2,6},{1,1}}, out=8},
{building="Rain Mill", stars=1, speed="01:24", quantities={{4,4,10},{3,3}}, out=8},
{building="Scribe", stars=3, speed="01:03", quantities={{2,2,6},{1,1}}, out=8} } },
["training gear"] = { product="Training Gear", pattern={{"Stone","Copper Bars","Crystalized Dew"},{"Planks","Reeds"}},
places={{building="Cooperage", stars=2, speed="02:06", quantities={{5,2,2},{3,3}}, out=10},
{building="Weaver", stars=1, speed="02:06", quantities={{8,3,3},{3,3}}, out=10},
{building="Tinkerer", stars=2, speed="02:06", quantities={{5,2,2},{3,3}}, out=10},
{building="Manufactory", stars=2, speed="02:06", quantities={{5,2,2},{3,3}}, out=10} } },
["wine"] = { product="Wine", pattern={{"Berries","Mushrooms","Reeds"},{"Pottery","Barrels","Waterskins"}},
places={{building="Cellar", stars=3, speed="01:03", quantities={{2,2,2},{2,2,2}}, out=10},
{building="Alchemist's Hut", stars=2, speed="01:24", quantities={{3,3,3},{3,3,3}}, out=10},
{building="Tinctury", stars=2, speed="01:24", quantities={{3,3,3},{3,3,3}}, out=10} } },
-- Crafting Materials
["clay"] = { product="Clay", pattern={{"Clearance Water"}},
places={{building="Clay Pit", stars=2, speed="01:24", quantities={{4}}, out=4} } },
-- no recipe for ["copper ore"] = { product="Copper Ore" },
["crystalized dew"] = { product="Crystalized Dew", pattern={{"Herbs","Insects","Resin","Vegetables"},{"Stone","Clay"},{"Storm Water","Clearance Water","Drizzle Water"}},
places={{building="Brickyard", stars=2, speed="00:42", quantities={{2,2,2,2},{3,3},{8,12,16}}, out=2},
{building="Alchemist's Hut", stars=2, speed="00:42", quantities={{2,2,2,2},{3,3},{8,12,16}}, out=2} } },
-- no recipe for ["grain"] = { product="Grain" },
["herbs"] = { product="Herbs", pattern={{"Drizzle Water"}},
places={{building="Greenhouse", stars=2, speed="01:24", quantities={{4}}, out=4} } },
["leather"] = { product="Leather", pattern={{"Plant Fiber","Reeds","Grain","Vegetables"}},
places={{building="Ranch", stars=1, speed="00:42", quantities={{2,2,2,1}}, out=4} } },
-- no recipe for ["plant fiber"] = { product="Plant Fiber" },
["reeds"] = { product="Reeds", pattern={{"Clearance Water"}},
places={{building="Clay Pit", stars=2, speed="01:24", quantities={{4}}, out=4} } },
-- no recipe for ["resin"] = { product="Resin" },
-- no recipe for ["sparkdew"] = { product="Sparkdew" },
-- no recipe for ["stone"] = { product="Stone" },
-- Refined Crafting Materials
["barrels"] = { product="Barrels", pattern={{"Copper Bars","Crystalized Dew"},{"Planks"}},
places={{building="Cooperage", stars=3, speed="01:45", quantities={{1,1},{2}}, out=10},
{building="Provisioner", stars=2, speed="02:06", quantities={{2,2},{2}}, out=10},
{building="Toolshop", stars=1, speed="02:06", quantities={{3,3},{2}}, out=10},
{building="Artisan", stars=2, speed="02:06", quantities={{2,2},{2}}, out=10} } },
["copper bars"] = { product="Copper Bars", pattern={{"Copper Ore"},{"Wood","Oil","Coal","Sea Marrow"}},
places={{building="Furnace", stars=2, speed="00:38", quantities={{5},{5,2,1,1}}, out=2},
{building="Grill", stars=1, speed="00:42", quantities={{6},{5,2,1,1}}, out=2},
{building="Stamping Mill", stars=1, speed="00:42", quantities={{6},{5,2,1,1}}, out=2},
{building="Smelter", stars=3, speed="00:28", quantities={{4},{5,2,1,1}}, out=2} } },
["flour"] = { product="Flour", pattern={{"Grain","Mushrooms","Roots"}},
places={{building="Press", stars=1, speed="00:53", quantities={{8,8,8}}, out=10},
{building="Flawless Rain Mill", stars=3, speed="01:03", quantities={{5,5,5}}, out=10},
{building="Rain Mill", stars=3, speed="01:03", quantities={{5,5,5}}, out=10},
{building="Provisioner", stars=2, speed="01:03", quantities={{7,7,7}}, out=10},
{building="Stamping Mill", stars=2, speed="01:03", quantities={{7,7,7}}, out=10},
{building="Supplier", stars=2, speed="01:03", quantities={{7,7,7}}, out=10} } },
["pigment"] = { product="Pigment", pattern={{"Insects","Berries","Copper Ore","Coal"}},
places={{building="Cookhouse", stars=2, speed="02:06", quantities={{4,4,4,3}}, out=10},
{building="Artisan", stars=2, speed="02:06", quantities={{4,4,4,3}}, out=10},
{building="Tinctury", stars=2, speed="02:06", quantities={{4,4,4,3}}, out=10},
{building="Manufactory", stars=2, speed="02:06", quantities={{4,4,4,3}}, out=10} } },
["pottery"] = { product="Pottery", pattern={{"Clay"},{"Wood","Oil","Coal","Sea Marrow"}},
places={{building="Bakery", stars=2, speed="01:24", quantities={{3},{5,2,1,1}}, out=5},
{building="Smokehouse", stars=1, speed="01:24", quantities={{4},{5,2,1,1}}, out=5},
{building="Brickyard", stars=2, speed="01:24", quantities={{3},{5,2,1,1}}, out=5},
{building="Stamping Mill", stars=3, speed="01:03", quantities={{2},{5,2,1,1}}, out=5} } },
["waterskins"] = { product="Waterskins", pattern={{"Leather"},{"Oil","Meat"}},
places={{building="Leatherworker", stars=3, speed="00:58", quantities={{4},{3,2}}, out=10},
{building="Clothier", stars=1, speed="01:24", quantities={{6},{5,4}}, out=10},
{building="Supplier", stars=2, speed="01:24", quantities={{5},{4,3}}, out=10} } },
-- Trade Goods
["amber"] = { product="Amber", pattern={{"Resin"},{"Clearance Water","Oil"}},
places={{building="Finesmith", stars=3, speed="00:42", quantities={{3},{10,1}}, out=1} } },
-- no recipe for ["ancient tablet"] = { product="Ancient Tablet" },
["pack of building materials"] = { product="Pack of Building Materials", pattern={{"Planks","Fabric","Bricks","Copper Ore"}},
places={{building="Makeshift Post", stars=0, speed="00:56", quantities={{10,6,6,14}}, out=2},
{building="Flawless Rain Mill", stars=3, speed="00:42", quantities={{6,3,3,8}}, out=2},
{building="Rain Mill", stars=1, speed="00:42", quantities={{8,5,5,12}}, out=2},
{building="Tinkerer", stars=2, speed="00:42", quantities={{7,4,4,10}}, out=2} } },
["pack of crops"] = { product="Pack of Crops", pattern={{"Roots","Grain","Vegetables","Mushrooms"}},
places={{building="Makeshift Post", stars=0, speed="00:56", quantities={{6,6,6,6}}, out=2},
{building="Granary", stars=2, speed="00:42", quantities={{4,4,4,4}}, out=2},
{building="Brewery", stars=1, speed="00:42", quantities={{5,5,5,5}}, out=2},
{building="Flawless Brewery", stars=3, speed="00:28", quantities={{3,3,3,3}}, out=5} } },
["pack of luxury goods"] = { product="Pack of Luxury Goods", pattern={{"Wine","Training Gear","Incense","Scrolls","Ale","Cosmetics"}},
places={{building="Carpenter", stars=2, speed="00:38", quantities={{4,4,4,4,4,4}}, out=2},
{building="Press", stars=1, speed="00:26", quantities={{5,5,5,5,5,5}}, out=2},
{building="Leatherworker", stars=1, speed="00:38", quantities={{5,5,5,5,5,5}}, out=2} } },
["pack of provisions"] = { product="Pack of Provisions", pattern={{"Herbs","Berries","Insects","Meat","Eggs"}},
places={{building="Makeshift Post", stars=0, speed="00:56", quantities={{6,6,6,6,6}}, out=3},
{building="Provisioner", stars=2, speed="00:42", quantities={{4,4,4,4,4}}, out=3},
{building="Manufactory", stars=2, speed="00:42", quantities={{4,4,4,4,4}}, out=3} } },
["pack of trade goods"] = { product="Pack of Trade Goods", pattern={{"Pigment","Oil","Flour","Pottery","Barrels","Waterskins"}},
places={{building="Lumber Mill", stars=0, speed="00:42", quantities={{8,8,6,6,6,6}}, out=2},
{building="Weaver", stars=0, speed="00:42", quantities={{8,8,6,6,6,6}}, out=2},
{building="Smithy", stars=0, speed="00:42", quantities={{6,6,4,4,4,4}}, out=2} } },
-- Meta resources
-- no recipe for ["artifacts"] = { product="Artifacts" },
-- no recipe for ["food stockpiles"] = { product="Food Stockpiles" },
-- no recipe for ["machinery"] = { product="Machinery" },
-- Fuel & Exploration
["coal"] = { product="Coal", pattern={{"Wood"}},
places={{building="Brick Oven", stars=1, speed="02:06", quantities={{15}}, out=3},
{building="Kiln", stars=3, speed="01:24", quantities={{10}}, out=5} } },
["oil"] = { product="Oil", pattern={{"Grain","Meat","Vegetables","Plant Fiber"}},
places={{building="Press", stars=3, speed="00:39", quantities={{2,2,2,2}}, out=5},
{building="Butcher", stars=2, speed="01:24", quantities={{3,3,3,3}}, out=5},
{building="Druid's Hut", stars=3, speed="01:03", quantities={{2,2,2,2}}, out=5} } },
-- no recipe for ["sea marrow"] = { product="Sea Marrow" },
-- no recipe for ["wood"] = { product="Wood" },
["simple tools"] = { product="Simple Tools", pattern={{"Wood","Planks"},{"Copper Bars","Crystalized Dew"}},
places={{building="Carpenter", stars=2, speed="01:30", quantities={{8,2},{3,3}}, out=2},
{building="Smithy", stars=2, speed="01:38", quantities={{8,2},{3,3}}, out=2},
{building="Toolshop", stars=3, speed="01:24", quantities={{6,1},{2,2}}, out=2},
{building="Tinkerer", stars=2, speed="01:38", quantities={{8,2},{3,3}}, out=2},
{building="Scribe", stars=1, speed="01:52", quantities={{10,3},{4,4}}, out=2} } },
["infused tools"] = { product="Infused Tools", pattern={{"Wood","Planks"},{"Copper Bars","Crystalized Dew"},{"Storm Water","Clearance Water","Drizzle Water"}},
places={{building="", stars=0, speed="", quantities={{6,1},{2,2},{8,12,16}}, out=2},
{building="", stars=0, speed="", quantities={{6,1},{2,2},{8,12,16}}, out=2} } }
}
-- no recipe for ["purging fire"] = { product="Purging Fire" },
-- Rain
-- no recipe for ["clearance water"] = { product="Clearance Water" },
-- no recipe for ["drizzle water"] = { product="Drizzle Water" },
-- no recipe for ["storm water"] = { product="Storm Water" }






-------------------------------------------------------------------------------
---
-- Main lookup functions
-- Retrieve all recipes that result in the specified product, and the buildings
-- Accepts the in-game name and returns the recipe associated with the
-- that produce them. With the following structure:
-- specified product.
--  
-------------------------------------------------------------------------------
-- 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






-- returns the whole recipe table for a given product
---
-- need to run normalize function first
-- Retrieve one specified recipe at the specified workshop. Returns an error if
-- this (currently) returns a reference, not a copy, so be careful not to change the data
-- no such combination exists. Two return values:
function RecipeData.getRecipeForProduct(argProductName)
--
-- 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]
if not argProductName then
local targetRecipeID = nil
return "Recipe_Data Error: product not given."
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
end
-- normalize input using resource normalizer method
    local strProductName = RecipeData.normalizeProductName(argProductName)
-- Get it from the big table above and return the whole pattern and places contents
if not targetRecipeID then
    return tableData[strProductName]
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
end






-- extract the pattern from the recipe table
---
-- hide the internals from calling modules
-- Retrieve all the recipes that the specified workshop can produce, with the
function RecipeData.getPatternFromRecipe(tRecipe)
-- following structure:
 
--
return tRecipe.pattern
-- 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
end






-- extract the recipe specifics for a given building.
---
-- this will return a reduced recipe table (following the specification above),
-- Retrieves all recipes for which the specified good is an ingredient, with the
-- with only one place, corresponding to the provided building.
-- following structure:
-- this function returns raw data, so other helper functions are necessary to
--
-- hide internals from calling methods.
-- ingredient good data { ... },
-- error handling will be necessary by calling function.
-- table of matches {
function RecipeData.getRecipeAtBuilding(argProductName, argBuildingName)
-- 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 argProductName then
if not recipeTable or not workshopTable or not goodsTable then
return "Recipe_Data Error: product not given."
loadRecipes()
end
end
if not argBuildingName then
return "Recipe_Data Error: building not given."
local targetIngredientID = findGoodIDByName(ingredientName)
if not targetIngredientID then
error("No ingredient found. Please check spelling and any punctuation like an apostrophe: " .. ingredientName)
end
end
-- normalize inputs using resource and building normalizer methods
local foundRecipeIDs = {}
    local strProductName = RecipeData.normalizeProductName(argProductName)
for recipeID, recipe in pairs(recipeTable) do
local strBuildingName = RecipeData.normalizeBuildingName(argBuildingName)
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
-- start with the table for the product, then find the building within its places
-- Expand the table to populate workshop IDs under each recipe ID
local tRecipe = tableData[strProductName]
local matches = findWorkshopsWithAnyOfTheseRecipes(foundRecipeIDs)
-- and make sure there's something to do
if not tRecipe then
return nil
end
-- build a separate, simplified table to return
-- Now build the nested table to return with the data
local tableToReturn = {}
local ingredMatches = {}
tableToReturn.product = tRecipe.product
for recipeID, match in pairs(matches) do
tableToReturn.pattern = tRecipe.pattern
-- loop over the places. once the building matches, copy just that place (reference) into the table to return
newMatch = {}
for _, place in ipairs(tRecipe.places) do
table.insert(newMatch, recipeTable[recipeID])
if string.lower(place.building) == strBuildingName then
tableToReturn.places = {place}
matchWorkshops = {}
return tableToReturn
for _, workshopID in ipairs(match) do
table.insert(matchWorkshops, workshopTable[workshopID])
end
end
table.insert(newMatch, matchWorkshops)
ingredMatches[recipeID] = newMatch
end
end
-- the building was not found, so return nothing
return goodsTable[targetIngredientID], ingredMatches
return nil
end
end






-- loop over all the places for all the recipes and get a list of all the recipes a building has
---
function RecipeData.getBuildingsRecipes(argBuildingName)
-- 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)
-- check input
if not workshopTable then
if not argBuildingName then
loadRecipes()
return "Recipe_Data Error: building not given."
end
end
-- normalize inputs using resource and building normalizer methods
local targetWorkshopID = findWorkshopIDByName(workshopName)
local strBuildingName = RecipeData.normalizeBuildingName(argBuildingName)
if not targetWorkshopID then
mw.log(strBuildingName)
error("No building found. Please check spelling and any punctuation like an apostrophe: " .. workshopName)
local recipesFoundHere = {}
end
mw.log("starting loop")
mw.logObject(tableData)
return workshopTable[targetWorkshopID]
mw.log("^^ on that mess")
end
-- loop through the recipes
 
for i,recipe in pairs(tableData) do --this needs to be pairs since everything at the top level of tableData has a string key
 
mw.log("####")
 
mw.log(i)
 
mw.logObject(recipe)
---
-- for each recipe, loop through the places
-- Transforms the oldRecipeTable returned from CSV processing to be more
for j, place in ipairs(recipe.places) do
-- conducive to member functions looking up data. Essentially, we convert the
mw.log("    ---")
-- text strings into tables with the same base name and an index.
mw.log(j)
--
mw.logObject(place)
-- Each item in newRecipeTable[i] looks like this:
-- once we find it, we can break out to the next recipe
-- recipe {
if place.building == strBuildingName then
-- id
mw.log("found it: building=" .. place.building)
-- efficiency grade
-- build a new recipe to add to recipesFoundHere
-- production time
local newRecipe = {}
-- product stack size
newRecipe.product = recipe.product
-- product good's id
newRecipe.pattern = recipe.pattern
-- ingredients {
newRecipe.places = {place}
-- options for ingredient #1 {
-- { option #1 good's stack size, option #1 good's id }
table.insert(recipesFoundHere,newRecipe)
-- { 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
break
end
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
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
return recipesFoundHere
return newWorkshopTable
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
-- Loops through the old workshop, extracting req'd goods and putting them
function RecipeData.getBuildingsAndStarsLists(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"
-- make sure we have something to work with
-- A subtable to return
if not tRecipe or not tRecipe.places or #tRecipe.places < 1 then
local requiredGoods = {}
return nil, nil
for i = 1,REQ_GOOD_MAX do
local oldIndex = workshopsHeaderLookup[LOOKUP_REQ_GOOD_BASE_STRING .. i]
local newGroup = {}
-- Split the digit part into the req'd good stack size and the rest
-- of the text into the req'd good's id.
newGroup[1], newGroup[2] = oldWorkshop[oldIndex]:match(PATTERN_SPLIT_STACK_AND_ID)
table.insert(requiredGoods, newGroup)
end
end
local strBuildings = {}
return requiredGoods
local intStars = {}
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)
-- loop through the places, extracting the strings and integers for the buildings and stars
-- A few constants we'll need only within this function.
-- and storing them in the tables that will be returned
local WORKPLACE_MAX = 4
for i, place in pairs(tRecipe.places) do
local LOOKUP_WORKPLACE_BASE_STRING = "workplace"
-- make sure no nil values or we don't need to continue
if not place.building or not place.stars then
-- A subtable to return
return nil, nil
local workplaces = {}
end
strBuildings[i] = place.building
for i = 1,WORKPLACE_MAX do
intStars[i] = place.stars
local oldIndex = workshopsHeaderLookup[LOOKUP_WORKPLACE_BASE_STRING .. i]
workplaces[i] = oldWorkshop[oldIndex]
end
end
return strBuildings, intStars
return workplaces
end
end






-- hide the internals of the above methods.
---
-- extract lists of ingredients and quantity for a specific recipe at a specific building
-- Loops through the old workshop, extracting recipes and putting them
function RecipeData.getIngredientsAndQuantities(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 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 = {}
-- make sure we have something to work with
for i=1,RECIPE_MAX do
if not tRecipe or not tRecipe.places or 0 == #tRecipe.places then
return nil, nil
local oldIndex = workshopsHeaderLookup[LOOKUP_RECIPE_BASE_STRING .. i]
recipes[i] = oldWorkshop[oldIndex]
end
end
return recipes
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
-- 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
end
return tRecipe.pattern, returnQuantities
return newGoodsTable
end
end






-- loop over the places and make a new list of extracting all the production times
---
function RecipeData.getProductionSpeed(tRecipe)
-- 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)
-- make sure we have something to work with
if not recipeTable or not workshopTable or not goodsTable then
if not tRecipe or not tRecipe.places or 0 == #tRecipe.places then
loadRecipes()
return nil
end
end
local speeds = {}
local foundGoodID = nil
for i, place in ipairs(tRecipe.places) do
-- Decide whether we need to traverse the big table. If this isn't the first
speeds[i] = place.speed
-- 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
end
return speeds
return foundGoodID
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
-- Look up so workshops can be found by their common ID, rather than their
function RecipeData.getProductAndNumbers(tRecipe)
-- 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


-- make sure we have something to work with
 
if not tRecipe or not tRecipe.places or 0 == #tRecipe.places then
 
return nil, nil
---
-- 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
end
local numbers = {}
local workshopIDsFound = {}
for _, targetRecID in ipairs(targetRecipeIDs) do
for i, place in ipairs(tRecipe.places) do
numbers[i] = place.out
workshopIDsFound[targetRecID] = findWorkshopsWithRecipe(targetRecID)
end
end
return tRecipe.product, numbers
return workshopIDsFound
end
end






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






-- Normalize the argument to the standard in-game name, and the one that
---
-- is used as the key in the big lookup table.
-- Retrieves the name and icon filename for a good.
--
--
-- These functions call external normalize methods.
-- @param goodID the ID of the good to look up
function RecipeData.normalizeProductName(strArg)
-- @return display name, icon filename
function RecipeData.getGoodNameAndIcon(goodID)
-- invoke the ResourceData.normalize method, since we're dealing in resource names
if not recipeTable or not workshopTable or not goodsTable then
return ResourceData.normalizeName(strArg)
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


function RecipeData.normalizeBuildingName(strArg)
 
 
---
-- 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
-- invoke the BuildingData.normalize method, since we're dealing in building names
return workshop[INDEX_WORKSHOP_NAME], workshop[INDEX_WORKSHOP_NAME] .. "_icon"
return BuildingData.normalizeName(strArg)
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