Module:RecipeData: Difference between revisions

From Against the Storm Official Wiki
m (added some logging for now)
(yet another typo in the new method; it was hard to unit test :/)
 
(19 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"


-- for returning when REQUIRE'd by other Lua modules.
local HEADER_ROW = 1
local RecipeData = {}
local DATA_ROWS = 2


local ResourceData = require("Module:ResourceData") -- need to reuse the normalize method
local PATTERN_SPLIT_STACK_AND_ID = "(%d+)%s([%[%]%s%a]+)"
local BuildingData = require("Module:BuildingData") -- need to reuse the normalize method


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
-- Constants
local INDEX_WORKSHOP_NAME = 2
-------------------------------------------------------------------------------
local INDEX_WORKSHOP_DESCRIPTION = 3
-- if needed
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




-------------------------------------------------------------------------------
-- 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.)
-- Private member variables
-- 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", 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 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
-- Main lookup functions
local workshopTable
-- Accepts the in-game name and returns the recipe associated with the
-- specified product.
-------------------------------------------------------------------------------


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


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


-- returns the whole recipe table for a given product
-- like this:  table[recipeID] = { workshopID, workshopID, workshopID }
-- need to run normalize function first
local recipeIDToWorkshopID
-- this (currently) returns a reference, not a copy, so be careful not to change the data
function RecipeData.getRecipeForProduct(argProductName)
if not argProductName then
return "Recipe_Data Error: product not given."
end


-- normalize input using resource normalizer method
-- like this:  table[display name] = goodID
    local strProductName = RecipeData.normalizeProductName(argProductName)
local goodsNameToGoodID
-- Get it from the big table above and return the whole pattern and places contents
    return tableData[strProductName]
end


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




-- extract the pattern from the recipe table
-- hide the internals from calling modules
function RecipeData.getPatternFromRecipe(tRecipe)


return tRecipe.pattern
---
-- 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
end






-- extract the recipe specifics for a given building.
---
-- this will return a reduced recipe table (following the specification above),
-- Retrieve all recipes that result in the specified product, and the buildings
-- with only one place, corresponding to the provided building.
-- that produce them. With the following structure:
-- 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(argProductName, argBuildingName)
-- 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 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 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
-- First find all the relevant recipes.
    local strProductName = RecipeData.normalizeProductName(argProductName)
local foundRecipeIDs = {}
local strBuildingName = RecipeData.normalizeBuildingName(argBuildingName)
for recipeID, recipe in pairs(recipeTable) do
-- start with the table for the product, then find the building within its places
if targetProductID == recipe[INDEX_RECIPE_PRODUCT_ID] then
local tRecipe = tableData[strProductName]
-- and make sure there's something to do
table.insert(foundRecipeIDs, recipeID)
if not tRecipe then
end
return nil
end
end
-- build a separate, simplified table to return
-- Now run the found recipes to get the workshops.
local tableToReturn = {}
local foundWorkshopIDs = findWorkshopsWithAnyOfTheseRecipes(foundRecipeIDs)
tableToReturn.product = tRecipe.product
tableToReturn.pattern = tRecipe.pattern
-- loop over the places. once the building matches, copy just that place (reference) into the table to return
-- Build the nested return table
for _, place in pairs(tRecipe.places) do
foundRecipes = {}
if string.lower(place.building) == strBuildingName then
for recipeID, listOfWorkshopIDs in pairs(foundWorkshopIDs) do
tableToReturn.places = {place}
return tableToReturn
-- 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
end
table.insert(match, workshops)
foundRecipes[recipeID] = match
end
end
-- the building was not found, so return nothing
return foundRecipes
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 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)
-- check input
if not recipeTable or not workshopTable or not goodsTable then
if not argBuildingName then
loadRecipes()
return "Recipe_Data Error: building not given."
end
end
-- normalize inputs using resource and building normalizer methods
local targetProductID = findGoodIDByName(productName)
local strBuildingName = RecipeData.normalizeBuildingName(argBuildingName)
if not targetProductID then
mw.log(strBuildingName)
error("No product found. Please check spelling and any punctuation like an apostrophe: " .. productName)
local recipesFoundHere = {}
end
mw.log("looping thru " .. #tableData)
-- loop through the recipes
local targetWorkshopID = findWorkshopIDByName(workshopName)
for i,recipe in ipairs(tableData) do
if not targetWorkshopID then
mw.log("####")
error("No building found. Please check spelling and any punctuation like an apostrophe: " .. workshopName)
mw.log(i)
end
mw.log(recipe)
-- for each recipe, loop through the places
local targetWorkshop = workshopTable[targetWorkshopID]
for j, place in ipairs(recipe.places) do
mw.log("    ---")
local targetRecipeID = nil
mw.log("    " .. j)
for _, recipeID in ipairs(targetWorkshop[INDEX_WORKSHOP_RECIPES]) do
mw.log("    " .. place)
-- once we find it, we can break out to the next recipe
if recipeID ~= "" and targetProductID == recipeTable[recipeID][INDEX_RECIPE_PRODUCT_ID] then
if place.building == strBuildingName then
targetRecipeID = recipeID
mw.log("found it: building=" .. place.building)
break
-- build a new recipe to add to recipesFoundHere
local newRecipe = {}
newRecipe.product = recipe.product
newRecipe.pattern = recipe.pattern
newRecipe.places = {place}
table.insert(recipesFoundHere,newRecipe)
break
end
end
end
end
end
return recipesFoundHere
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
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
-- Retrieve all the recipes that the specified workshop can produce, with the
function RecipeData.getBuildingsAndStarsLists(tRecipe)
-- 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
-- make sure we have something to work with
local targetWorkshopID = findWorkshopIDByName(workshopName)
if not tRecipe or not tRecipe.places or #tRecipe.places < 1 then
if not targetWorkshopID then
return nil, nil
error("No building found. Please check spelling and any punctuation like an apostrophe: " .. workshopName)
end
end
local strBuildings = {}
local targetWorkshop = workshopTable[targetWorkshopID]
local intStars = {}
-- Reuse this subtable for each recipe.
local nestedWorkshopList = {}
table.insert(nestedWorkshopList, targetWorkshop)
-- loop through the places, extracting the strings and integers for the buildings and stars
local matches = {}
-- and storing them in the tables that will be returned
for _, recipeID in ipairs(targetWorkshop[INDEX_WORKSHOP_RECIPES]) do
for i, place in pairs(tRecipe.places) do
-- make sure no nil values or we don't need to continue
if recipeID ~= "" then
if not place.building or not place.stars then
return nil, nil
local newMatch = {}
table.insert(newMatch,recipeTable[recipeID])
table.insert(newMatch, nestedWorkshopList)
matches[recipeID] = newMatch
end
end
strBuildings[i] = place.building
intStars[i] = place.stars
end
end
return strBuildings, intStars
return targetWorkshop, matches
end
end






-- hide the internals of the above methods.
---
-- extract lists of ingredients and quantity for a specific recipe at a specific building
-- Retrieves all recipes for which the specified good is an ingredient, with the
function RecipeData.getIngredientsAndQuantities(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)
-- 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, nil
end
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


-- 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
-- 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
end
return tRecipe.pattern, returnQuantities
return workshopTable[targetWorkshopID]
end
end






-- loop over the places and make a new list of extracting all the production times
 
function RecipeData.getProductionSpeed(tRecipe)
---
-- 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)
-- make sure we have something to work with
-- New table structure is only data rows, no header row needed. Therefore,
if not tRecipe or not tRecipe.places or 0 == #tRecipe.places then
-- the new table is one degree flatter, like this:
return nil
-- 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
local speeds = {}
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"
for i, place in ipairs(tRecipe.places) do
-- A subtable for the lists of ingredients and their options.
speeds[i] = place.speed
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
end
return speeds
return ingredients
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
-- Transforms the oldWorkshopsTable returned from CSV processing to be more
function RecipeData.getProductAndNumbers(tRecipe)
-- 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
 
 


-- make sure we have something to work with
---
if not tRecipe or not tRecipe.places or 0 == #tRecipe.places then
-- Loops through the old workshop, extracting req'd goods and putting them
return nil, nil
-- 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
end
local numbers = {}
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)
for i, place in ipairs(tRecipe.places) do
-- A few constants we'll need only within this function.
numbers[i] = place.out
local RECIPE_MAX = 4
local LOOKUP_RECIPE_BASE_STRING = "recipe"
-- A subtable to return
local recipes = {}
for i=1,RECIPE_MAX do
local oldIndex = workshopsHeaderLookup[LOOKUP_RECIPE_BASE_STRING .. i]
recipes[i] = oldWorkshop[oldIndex]
end
end
return tRecipe.product, numbers
return recipes
end
end






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






-- 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 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.
--
--
-- These functions call external normalize methods.
-- @param displayName the plain-language name of the good to find
function RecipeData.normalizeProductName(strArg)
-- @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
-- invoke the ResourceData.normalize method, since we're dealing in resource names
return foundGoodID
return ResourceData.normalizeName(strArg)
end
end


function RecipeData.normalizeBuildingName(strArg)
 
 
---
-- 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
-- invoke the BuildingData.normalize method, since we're dealing in building names
return foundWorkshopID
return BuildingData.normalizeName(strArg)
end
end






-- accepts a 2x2 table in the format of pattern and quantities in tableData above
---
function RecipeData.copyPatternTypeTable(tArg)
-- 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
 
 


local tReturn = {}
---
-- 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)
for k, v in ipairs(tArg) do
if not recipeTable or not workshopTable or not goodsTable then
for ik, iv in ipairs(tArg[k]) do
loadRecipes()
tReturn[k][ik] = iv
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
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)
tReturn["building"] = tArg["building"]
if not workshopTable then
tReturn["stars"] = tArg["stars"]
loadRecipes()
tReturn["speed"] = tArg["speed"]
end
tReturn["out"] = tArg["out"]
tReturn["places"] = copyPatternTypeTable(tArg["places"])
local workshop = workshopTable[workshopID]
 
return tReturn
if not workshop then
error("ID for workshop not found to look up name and icon: " .. workshopID)
end
return workshop[INDEX_WORKSHOP_NAME], workshop[INDEX_WORKSHOP_NAME] .. "_icon"
end
end






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