Module:RecipeData: Difference between revisions
From Against the Storm Official Wiki
m (removed some logging) |
(yet another typo in the new method; it was hard to unit test :/) |
||
(13 intermediate revisions by the same 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 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 | local workshopTable | ||
-- like this: table[goodID] = table containing good data | |||
local | 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. | ||
-- the | -- | ||
-- 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: | ||
function RecipeData. | -- | ||
-- 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 | if not recipeTable or not workshopTable or not goodsTable then | ||
loadRecipes() | |||
end | 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 | end | ||
-- | --- | ||
-- | -- Retrieve all the recipes that the specified workshop can produce, with the | ||
function RecipeData. | -- following structure: | ||
-- | |||
return | -- workshop, | ||
-- table of matches { | |||
-- match[recipeID] between recipe and workshops = { | |||
-- recipe data { ... }, | |||
-- workshops that make it { | |||
-- workshop data { ... } | |||
-- } | |||
-- }, | |||
-- match[recipeID] between recipe and workshops = { | |||
-- recipe data { ... }, | |||
-- workshops that make it { | |||
-- workshop data { ... } | |||
-- } | |||
-- } | |||
-- } | |||
-- | |||
-- @param workshopName the plain language display name of the workshop | |||
-- @return list of recipes at that building | |||
function RecipeData.getAllRecipesAtBuilding(workshopName) | |||
if not recipeTable or not workshopTable or not goodsTable then | |||
loadRecipes() | |||
end | |||
local targetWorkshopID = findWorkshopIDByName(workshopName) | |||
if not targetWorkshopID then | |||
error("No building found. Please check spelling and any punctuation like an apostrophe: " .. workshopName) | |||
end | |||
local targetWorkshop = workshopTable[targetWorkshopID] | |||
-- Reuse this subtable for each recipe. | |||
local nestedWorkshopList = {} | |||
table.insert(nestedWorkshopList, targetWorkshop) | |||
local matches = {} | |||
for _, recipeID in ipairs(targetWorkshop[INDEX_WORKSHOP_RECIPES]) do | |||
if recipeID ~= "" then | |||
local newMatch = {} | |||
table.insert(newMatch,recipeTable[recipeID]) | |||
table.insert(newMatch, nestedWorkshopList) | |||
matches[recipeID] = newMatch | |||
end | |||
end | |||
return targetWorkshop, matches | |||
end | end | ||
-- | --- | ||
-- | -- Retrieves all recipes for which the specified good is an ingredient, with the | ||
-- | -- following structure: | ||
-- | -- | ||
-- | -- ingredient good data { ... }, | ||
-- | -- table of matches { | ||
function RecipeData. | -- 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 | |||
if not | local targetIngredientID = findGoodIDByName(ingredientName) | ||
if not targetIngredientID then | |||
error("No ingredient found. Please check spelling and any punctuation like an apostrophe: " .. ingredientName) | |||
end | end | ||
if | |||
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 | ||
-- | -- Expand the table to populate workshop IDs under each recipe ID | ||
local matches = findWorkshopsWithAnyOfTheseRecipes(foundRecipeIDs) | |||
local | |||
-- | -- 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 | 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 | end | ||
return workshopTable[targetWorkshopID] | |||
end | end | ||
-- | |||
function | --- | ||
-- 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 newRecipeTable | |||
end | |||
--- | |||
-- Loops through the old recipe, extracting ingredient options and putting them | |||
-- into new subtables. Uses the lookup table from the CSV extraction to | |||
-- keep the data in the same order. | |||
-- | |||
-- @param oldRecipe the recipe from which to extract ingredient information | |||
-- @param recipeHeaderLookup the lookup table built from the CSV data | |||
-- @return a subtable with the ingredients information from oldRecipe | |||
function makeIngredientsSubtable(oldRecipe, recipeHeaderLookup) | |||
-- A few constants we'll need only within this function. | |||
local RECIPE_INGRED_MAX = 3 | |||
local RECIPE_INGRED_OPTION_MAX = 6 | |||
local LOOKUP_INGREDIENT_BASE_STRING = "ingredient" | |||
local LOOKUP_OPTION_BASE_STRING = "Good" | |||
-- A subtable for the lists of ingredients and their options. | |||
local ingredients = {} | |||
for i = 1,RECIPE_INGRED_MAX do | |||
-- A subtable for the options for this ingredient. | |||
local options = {} | |||
for j = 1,RECIPE_INGRED_OPTION_MAX do | |||
local oldIndex = recipeHeaderLookup[LOOKUP_INGREDIENT_BASE_STRING .. i .. LOOKUP_OPTION_BASE_STRING .. j] | |||
-- Once it gets to a blank, can advance to the next group | |||
local oldOption = oldRecipe[oldIndex] | |||
if oldOption == "" then | |||
break | break | ||
end | end | ||
-- Split the digit part into the ingredient stack size and the rest | |||
-- of the text into the ingredient good's id. | |||
local stackSize, goodID = oldOption:match(PATTERN_SPLIT_STACK_AND_ID) | |||
table.insert(options, {stackSize, goodID}) | |||
end | end | ||
-- Add finished options list to represent the ith ingredient | |||
ingredients[i] = options | |||
end | end | ||
return | return ingredients | ||
end | end | ||
-- | --- | ||
-- | -- Transforms the oldWorkshopsTable returned from CSV processing to be more | ||
function | -- conducive to member functions looking up data. Essentially, we convert the | ||
-- text strings into tables with the same base name and an index. | |||
-- | |||
-- Each item newWorkshopTable[i] looks like this: | |||
-- workshop { | |||
-- id | |||
-- display name | |||
-- description | |||
-- category | |||
-- size x | |||
-- size y | |||
-- city score | |||
-- movable | |||
-- initially essential | |||
-- storage | |||
-- construction time | |||
-- required goods { | |||
-- #1 { stack size, id } | |||
-- #2 { stack size, id } | |||
-- #3 { stack size, id } | |||
-- } | |||
-- workplaces { | |||
-- workplace #1 | |||
-- workplace #2 | |||
-- workplace #3 | |||
-- workplace #4 | |||
-- } | |||
-- recipes { | |||
-- recipe #1 | |||
-- recipe #2 | |||
-- recipe #3 | |||
-- recipe #4 | |||
-- } | |||
-- } | |||
-- | |||
-- @param oldRecipeTable the CSV-based table, with a header row then data rows | |||
-- @param recipeHeaderLookup the lookup table built from the CSV data | |||
-- @return a new table for use in looking up recipe data | |||
function restructureWorkshopTable(oldWorkshopTable, workshopsHeaderLookup) | |||
-- New table structure is only data rows, no header row needed. Therefore, | |||
-- the new table is one degree flatter, like this: | |||
-- oldWorkshopTable[2][1] contains the same data as newWorkshopTable[1] | |||
-- (but organized by workshopID instead of by index and with subtables) | |||
local newWorkshopTable = {} | |||
oldWorkshopTable = oldWorkshopTable[DATA_ROWS] | |||
-- A few constants we need only in this function. | |||
local INDEX_OLD_WORKSHOP_ID = 1 | |||
local INDEX_OLD_WORKSHOP_NAME = 2 | |||
local INDEX_OLD_WORKSHOP_DESCRIPTION = 3 | |||
local INDEX_OLD_WORKSHOP_CATEGORY = 4 | |||
local INDEX_OLD_WORKSHOP_SIZE_X = 5 | |||
local INDEX_OLD_WORKSHOP_SIZE_Y = 6 | |||
local INDEX_OLD_WORKSHOP_CONSTRUCTION_TIME = 10 | |||
local INDEX_OLD_WORKSHOP_CITY_SCORE = 11 | |||
local INDEX_OLD_WORKSHOP_MOVABLE = 12 | |||
local INDEX_OLD_WORKSHOP_INITIALLY_ESSENTIAL = 13 | |||
local INDEX_OLD_WORKSHOP_STORAGE = 14 | |||
-- | for i, oldWorkshop in ipairs(oldWorkshopTable) do | ||
local newWorkshop = {} | |||
-- Copy over the flat information from the old recipe. | |||
local newWorkshopID = oldWorkshop[INDEX_OLD_WORKSHOP_ID] | |||
newWorkshop[INDEX_WORKSHOP_ID] = newWorkshopID | |||
newWorkshop[INDEX_WORKSHOP_NAME] = oldWorkshop[INDEX_OLD_WORKSHOP_NAME] | |||
newWorkshop[INDEX_WORKSHOP_DESCRIPTION] = oldWorkshop[INDEX_OLD_WORKSHOP_DESCRIPTION] | |||
newWorkshop[INDEX_WORKSHOP_CATEGORY] = oldWorkshop[INDEX_OLD_WORKSHOP_CATEGORY] | |||
newWorkshop[INDEX_WORKSHOP_SIZE_X] = oldWorkshop[INDEX_OLD_WORKSHOP_SIZE_X] | |||
newWorkshop[INDEX_WORKSHOP_SIZE_Y] = oldWorkshop[INDEX_OLD_WORKSHOP_SIZE_Y] | |||
newWorkshop[INDEX_WORKSHOP_CITY_SCORE] = oldWorkshop[INDEX_OLD_WORKSHOP_CITY_SCORE] | |||
newWorkshop[INDEX_WORKSHOP_MOVABLE] = oldWorkshop[INDEX_OLD_WORKSHOP_MOVABLE] | |||
newWorkshop[INDEX_WORKSHOP_INITIALLY_ESSENTIAL] = oldWorkshop[INDEX_OLD_WORKSHOP_INITIALLY_ESSENTIAL] | |||
newWorkshop[INDEX_WORKSHOP_STORAGE] = oldWorkshop[INDEX_OLD_WORKSHOP_STORAGE] | |||
newWorkshop[INDEX_WORKSHOP_CONSTRUCTION_TIME] = oldWorkshop[INDEX_OLD_WORKSHOP_CONSTRUCTION_TIME] | |||
newWorkshop[INDEX_WORKSHOP_REQUIRED_GOODS] = makeRequiredGoodsSubtable(oldWorkshop, workshopsHeaderLookup) | |||
newWorkshop[INDEX_WORKSHOP_WORKPLACES] = makeWorkplacesSubtable(oldWorkshop, workshopsHeaderLookup) | |||
newWorkshop[INDEX_WORKSHOP_RECIPES] = makeRecipesSubtable(oldWorkshop, workshopsHeaderLookup) | |||
newWorkshopTable[newWorkshopID] = newWorkshop | |||
end | end | ||
local | return newWorkshopTable | ||
local | 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, | |||
for i = 1,WORKPLACE_MAX do | |||
local oldIndex = workshopsHeaderLookup[LOOKUP_WORKPLACE_BASE_STRING .. i] | |||
workplaces[i] = oldWorkshop[oldIndex] | |||
end | end | ||
return | return workplaces | ||
end | end | ||
-- | --- | ||
-- extract | -- Loops through the old workshop, extracting recipes and putting them | ||
function | -- into a new subtable. Uses the lookup table from the CSV extraction to | ||
-- keep the data in the same order. | |||
-- | |||
-- @param oldWorkshop the workshop from which to extract recipes | |||
-- @param workshopsHeaderLookup the lookup table built from the CSV data | |||
-- @return a subtable with the recipes from oldWorkshop | |||
function makeRecipesSubtable(oldWorkshop, workshopsHeaderLookup) | |||
-- A few constants we'll need only within this function. | |||
local RECIPE_MAX = 4 | |||
local LOOKUP_RECIPE_BASE_STRING = "recipe" | |||
-- A subtable to return | |||
local recipes = {} | |||
for i=1,RECIPE_MAX do | |||
local oldIndex = workshopsHeaderLookup[LOOKUP_RECIPE_BASE_STRING .. i] | |||
recipes[i] = oldWorkshop[oldIndex] | |||
end | end | ||
return recipes | |||
end | |||
local | |||
for | --- | ||
-- Since the goods information is already flat and well structured, all this has | |||
-- to do is swap out the integer keys for the good IDs and only return the data | |||
-- rows, without the header row. | |||
-- | |||
-- @param oldGoodsTable the CSV-based table, with a header row then data rows | |||
-- @param goodsHeaderLookup the lookup table built from the CSV data | |||
-- @return a new table for use in looking up goods data | |||
function restructureGoodsTable(oldGoodsTable, goodsHeaderLookup) | |||
local newGoodsTable = {} | |||
for _, good in ipairs(oldGoodsTable[DATA_ROWS]) do | |||
newGoodsTable[good[INDEX_GOOD_ID]] = good | |||
end | end | ||
return | return newGoodsTable | ||
end | end | ||
-- | --- | ||
function | -- 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 | |||
if not | loadRecipes() | ||
end | end | ||
local | local foundGoodID = nil | ||
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 goodsNameToGoodID then | |||
goodsNameToGoodID = {} | |||
for goodID, good in pairs(goodsTable) do | |||
if not foundGoodID and good[INDEX_GOOD_NAME] == displayName then | |||
-- Found it, keep traversing to build the rest of the map. | |||
foundGoodID = goodID | |||
end | |||
goodsNameToGoodID[good[INDEX_GOOD_NAME]] = goodID | |||
end | |||
else | |||
-- From the lookup table. | |||
foundGoodID = goodsNameToGoodID[displayName] | |||
end | end | ||
return | return foundGoodID | ||
end | 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 | |||
--- | |||
if not | -- Expand the table provided by running each recipe through the other method | ||
-- that returns all workshops for one recipe. | |||
-- | |||
-- @param targetRecipes a table of recipe IDs to find separately | |||
-- @return a table of workshops | |||
function findWorkshopsWithAnyOfTheseRecipes(targetRecipeIDs) | |||
if not recipeTable or not workshopTable or not goodsTable then | |||
loadRecipes() | |||
end | end | ||
local | local workshopIDsFound = {} | ||
for _, targetRecID in ipairs(targetRecipeIDs) do | |||
for | |||
workshopIDsFound[targetRecID] = findWorkshopsWithRecipe(targetRecID) | |||
end | end | ||
return | return workshopIDsFound | ||
end | 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 | ||
function RecipeData. | -- @return display name, icon filename | ||
function RecipeData.getGoodNameAndIcon(goodID) | |||
if not recipeTable or not workshopTable or not goodsTable then | |||
return | loadRecipes() | ||
end | |||
local good = goodsTable[goodID] | |||
if not good then | |||
error("ID for good not found to look up name and icon: " .. goodID) | |||
end | |||
return good[INDEX_GOOD_NAME], good[INDEX_GOOD_ICON_FILENAME] | |||
end | end | ||
function RecipeData. | |||
--- | |||
-- 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 | end | ||
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