Module:RecipeData: Difference between revisions
From Against the Storm Official Wiki
trying to get allRecipes work |
Anteprefix (talk | contribs) No edit summary |
||
| (27 intermediate revisions by one other user not shown) | |||
| Line 1: | Line 1: | ||
--- | |||
-- | -- 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 | 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 | |||
-- | --- | ||
-- need | -- 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) | |||
-- New, optimized data loading structure | |||
allTables = mw.loadData("Module:RecipeData/Data") | |||
recipeTable = allTables[1] | |||
recipeIDToWorkshopID = allTables[2] | |||
workshopTable = allTables[3] | |||
workshopNameToWorkshopID = allTables[4] | |||
goodsTable = allTables[5] | |||
goodsNameToGoodID = allTables[6] | |||
end | end | ||
-- | --- | ||
-- | -- Retrieve all recipes that result in the specified product, and the buildings | ||
function RecipeData. | -- that produce them. With the following structure: | ||
-- | |||
return | -- table of matches { | ||
-- match[recipeID] between recipe and workshops = { | |||
-- recipe data { ... } | |||
-- workshops that make it { | |||
-- workshop data { ... } | |||
-- workshop data { ... } | |||
-- workshop data { ... } | |||
-- } | |||
-- } | |||
-- match[recipeID] between recipe and workshops = { | |||
-- ... | |||
-- } | |||
-- } | |||
-- | |||
-- @param productName plain language name of the product (good) | |||
-- @return all recipes that result in the product, and all workshops that | |||
-- produce those recipes, in a nested table | |||
function RecipeData.getAllRecipesForProduct(productName) | |||
if not recipeTable or not workshopTable or not goodsTable then | |||
loadRecipes() | |||
end | |||
local targetProductID = findGoodIDByName(productName) | |||
if not targetProductID then | |||
error("No product found. Please check spelling and any punctuation like an apostrophe: " .. productName) | |||
end | |||
-- First find all the relevant recipes. | |||
local foundRecipeIDs = {} | |||
for recipeID, recipe in pairs(recipeTable) do | |||
if targetProductID == recipe[INDEX_RECIPE_PRODUCT_ID] then | |||
table.insert(foundRecipeIDs, recipeID) | |||
end | |||
end | |||
-- Now run the found recipes to get the workshops. | |||
local foundWorkshopIDs = findWorkshopsWithAnyOfTheseRecipes(foundRecipeIDs) | |||
-- Build the nested return table | |||
foundRecipes = {} | |||
for recipeID, listOfWorkshopIDs in pairs(foundWorkshopIDs) do | |||
-- Make a new table to store the match between the recipe and the | |||
-- buildings that make it | |||
local match = {} | |||
table.insert(match, recipeTable[recipeID]) | |||
local workshops = {} | |||
for i, workshopID in pairs(listOfWorkshopIDs) do | |||
table.insert(workshops, workshopTable[workshopID]) | |||
end | |||
table.insert(match, workshops) | |||
foundRecipes[recipeID] = match | |||
end | |||
return foundRecipes | |||
end | end | ||
-- | --- | ||
-- | -- 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 = { | ||
function RecipeData. | -- 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 | 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 | end | ||
if not | |||
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 | 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) | |||
local | if not targetWorkshopID then | ||
error("No building found. Please check spelling and any punctuation like an apostrophe: " .. workshopName) | |||
if not | |||
end | end | ||
-- | local targetWorkshop = workshopTable[targetWorkshopID] | ||
local | -- Reuse this subtable for each recipe. | ||
local nestedWorkshopList = {} | |||
table.insert(nestedWorkshopList, targetWorkshop) | |||
local matches = {} | |||
for _, | for _, recipeID in ipairs(targetWorkshop[INDEX_WORKSHOP_RECIPES]) do | ||
if | |||
if recipeID ~= "" then | |||
local newMatch = {} | |||
table.insert(newMatch,recipeTable[recipeID]) | |||
table.insert(newMatch, nestedWorkshopList) | |||
matches[recipeID] = newMatch | |||
end | end | ||
end | end | ||
return targetWorkshop, matches | |||
end | end | ||
-- | --- | ||
-- | -- Retrieves all recipes for which the specified good is an ingredient, with the | ||
function RecipeData. | -- 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 | |||
if not | loadRecipes() | ||
end | end | ||
local | 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 | ||
end | end | ||
return | -- 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 | end | ||
-- | --- | ||
-- | -- Retrieve all data for the specified workshop. This was originally the main | ||
function RecipeData. | -- 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 | if not targetWorkshopID then | ||
error("No building found. Please check spelling and any punctuation like an apostrophe: " .. workshopName) | |||
end | end | ||
return workshopTable[targetWorkshopID] | |||
end | |||
local | |||
for i, | |||
--- | |||
-- Transforms the oldRecipeTable returned from CSV processing to be more | |||
-- conducive to member functions looking up data. Essentially, we convert the | |||
-- text strings into tables with the same base name and an index. | |||
-- | |||
-- Each item in newRecipeTable[i] looks like this: | |||
-- recipe { | |||
-- id | |||
-- efficiency grade | |||
-- production time | |||
-- product stack size | |||
-- product good's id | |||
-- ingredients { | |||
-- options for ingredient #1 { | |||
-- { option #1 good's stack size, option #1 good's id } | |||
-- { option #2 good's stack size, option #2 good's id } | |||
-- { option #3 good's stack size, option #3 good's id } | |||
-- etc. | |||
-- } | |||
-- options for ingredient #2 { | |||
-- same as above | |||
-- } | |||
-- options for ingredient #3 { | |||
-- same as above | |||
-- } | |||
-- } | |||
-- } | |||
-- | |||
-- @param oldRecipeTable the CSV-based table, with a header row then data rows | |||
-- @param recipeHeaderLookup the lookup table built from the CSV data | |||
-- @return a new table for use in looking up recipe data | |||
function restructureRecipeTable(oldRecipeTable, recipeHeaderLookup) | |||
-- New table structure is only data rows, no header row needed. Therefore, | |||
-- the new table is one degree flatter, like this: | |||
-- oldRecipeTable[2][1] contains the same data as newRecipeTable[1] | |||
-- (but with subtables) | |||
local newRecipeTable = {} | |||
oldRecipeTable = oldRecipeTable[DATA_ROWS] | |||
-- A few constants we need only in this function | |||
local INDEX_OLD_RECIPE_ID = 1 | |||
local INDEX_OLD_RECIPE_GRADE = 2 | |||
local INDEX_OLD_RECIPE_PRODTIME = 3 | |||
local INDEX_OLD_RECIPE_PRODUCT = 4 | |||
for i, oldRecipe in ipairs(oldRecipeTable) do | |||
local newRecipe = {} | |||
-- Copy over the flat information from the old recipe. | |||
local newRecipeID = oldRecipe[INDEX_OLD_RECIPE_ID] | |||
newRecipe[INDEX_RECIPE_ID] = newRecipeID | |||
newRecipe[INDEX_RECIPE_GRADE] = oldRecipe[INDEX_OLD_RECIPE_GRADE] | |||
newRecipe[INDEX_RECIPE_PRODTIME] = oldRecipe[INDEX_OLD_RECIPE_PRODTIME] | |||
-- Split the digit part into the product stack size and the rest of the | |||
-- text into the product good's id. | |||
newRecipe[INDEX_RECIPE_PRODUCT_STACK_SIZE], newRecipe[INDEX_RECIPE_PRODUCT_ID] = oldRecipe[INDEX_OLD_RECIPE_PRODUCT]:match(PATTERN_SPLIT_STACK_AND_ID) | |||
newRecipe[INDEX_RECIPE_INGREDIENTS] = makeIngredientsSubtable(oldRecipe,recipeHeaderLookup) | |||
newRecipeTable[newRecipeID] = newRecipe | |||
end | end | ||
return | return newRecipeTable | ||
end | end | ||
-- | --- | ||
function | -- 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 | end | ||
local | 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, | 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 | return newWorkshopTable | ||
end | 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 | end | ||
local | return workplaces | ||
end | |||
--- | |||
-- Loops through the old workshop, extracting recipes and putting them | |||
-- into a new subtable. Uses the lookup table from the CSV extraction to | |||
-- keep the data in the same order. | |||
-- | |||
-- @param oldWorkshop the workshop from which to extract recipes | |||
-- @param workshopsHeaderLookup the lookup table built from the CSV data | |||
-- @return a subtable with the recipes from oldWorkshop | |||
function makeRecipesSubtable(oldWorkshop, workshopsHeaderLookup) | |||
-- A few constants we'll need only within this function. | |||
local RECIPE_MAX = 4 | |||
local LOOKUP_RECIPE_BASE_STRING = "recipe" | |||
for i, | -- 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 | return recipes | ||
end | end | ||
--- | |||
-- Since the goods information is already flat and well structured, all this has | |||
-- to do is swap out the integer keys for the good IDs and only return the data | |||
-- rows, without the header row. | |||
-- | |||
-- @param oldGoodsTable the CSV-based table, with a header row then data rows | |||
-- @param goodsHeaderLookup the lookup table built from the CSV data | |||
-- @return a new table for use in looking up goods data | |||
function restructureGoodsTable(oldGoodsTable, goodsHeaderLookup) | |||
local newGoodsTable = {} | |||
for _, good in ipairs(oldGoodsTable[DATA_ROWS]) do | |||
newGoodsTable[good[INDEX_GOOD_ID]] = good | |||
end | |||
return newGoodsTable | |||
end | |||
-------------------------------- | --- | ||
-- 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 | ||
function | -- @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 | end | ||
function | |||
--- | |||
-- 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 = {} | |||
return | for _, targetRecID in ipairs(targetRecipeIDs) do | ||
workshopIDsFound[targetRecID] = findWorkshopsWithRecipe(targetRecID) | |||
end | |||
return workshopIDsFound | |||
end | end | ||
-- | --- | ||
function | -- Compiles the records for the workshops that can produce the specified | ||
-- recipe. Returns the whole workshop record, so it can be filtered out by | |||
local | -- the calling method if needed, or not. | ||
-- | |||
-- @param targetRecipeID the id of the recipe to find | |||
-- @return a table of workshop records | |||
function findWorkshopsWithRecipe(targetRecipeID) | |||
if not recipeTable or not workshopTable or not goodsTable then | |||
loadRecipes() | |||
end | |||
local workshopIDsFound = {} | |||
for | -- 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 | return workshopIDsFound | ||
end | end | ||
-- | --- | ||
function RecipeData. | -- 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] | |||
return | 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 RecipeData | return RecipeData | ||
Latest revision as of 20:40, 31 August 2025
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)
-- New, optimized data loading structure
allTables = mw.loadData("Module:RecipeData/Data")
recipeTable = allTables[1]
recipeIDToWorkshopID = allTables[2]
workshopTable = allTables[3]
workshopNameToWorkshopID = allTables[4]
goodsTable = allTables[5]
goodsNameToGoodID = allTables[6]
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


