Module:BaseDataModel: Difference between revisions
From Against the Storm Official Wiki
m (added recipe data interface) |
(Adding method to get product + ingredient combos) |
||
(16 intermediate revisions by the same user not shown) | |||
Line 24: | Line 24: | ||
local INDEX_RECIPES = "recipes" | local INDEX_RECIPES = "recipes" | ||
local INDEX_RECIPE_GRADE = "grade" | local INDEX_RECIPE_GRADE = "grade" | ||
local INDEX_RECIPE_GRADE_ALT = "gradeId" | |||
local INDEX_RECIPE_INGREDIENTS = "ingredients" | local INDEX_RECIPE_INGREDIENTS = "ingredients" | ||
local INDEX_RECIPE_INGREDIENT_OPTION_AMOUNT = "amount" | local INDEX_RECIPE_INGREDIENT_OPTION_AMOUNT = "amount" | ||
local INDEX_RECIPE_INGREDIENT_OPTION_ID = "name" | local INDEX_RECIPE_INGREDIENT_OPTION_ID = "name" | ||
local INDEX_RECIPE_ING_SERVICE_GOODS = "goods" -- alt to ingredients for service buildings | |||
local INDEX_RECIPE_PRODUCT = "product" | local INDEX_RECIPE_PRODUCT = "product" | ||
local INDEX_RECIPE_PRODUCT_AMOUNT = "amount" | local INDEX_RECIPE_PRODUCT_AMOUNT = "amount" | ||
local INDEX_RECIPE_PRODUCT_ID = "name" | local INDEX_RECIPE_PRODUCT_ID = "name" | ||
local | local INDEX_RECIPE_PRO_DEPOSITS = "seekedDeposits" -- alt to product for huts and camps | ||
local INDEX_RECIPE_PRO_SERVICE = "servedNeed" -- alt to product for service buildings | |||
local INDEX_RECIPE_PRODUCTION_TIME = "productionTime" | |||
local INDEX_RECIPE_PLANTING_TIME = "plantingTime" | |||
local INDEX_RECIPE_HARVESTING_TIME = "harvestingTime" | |||
local INDEX_RECIPE_GATHERING_TIME = "gatheringTime" | |||
local INDEX_CONSTRUCTION_GOODS = "requiredGoods" | local INDEX_CONSTRUCTION_GOODS = "requiredGoods" | ||
local INDEX_CONSTRUCTION_GOODS_AMOUNT = "amount" | local INDEX_CONSTRUCTION_GOODS_AMOUNT = "amount" | ||
Line 37: | Line 44: | ||
local INDEX_SIZE_Y = "sizeY" | local INDEX_SIZE_Y = "sizeY" | ||
local INDEX_STORAGE_CAP = "storage" | local INDEX_STORAGE_CAP = "storage" | ||
local INDEX_STORAGE_TANK = "baseTankCapacity" | |||
local INDEX_WORKPLACES = "workplaces" | local INDEX_WORKPLACES = "workplaces" | ||
Line 290: | Line 298: | ||
local building = findID(self, id) | local building = findID(self, id) | ||
if building then | if building then | ||
return building[INDEX_STORAGE_CAP] | if building[INDEX_STORAGE_CAP] then | ||
return building[INDEX_STORAGE_CAP] | |||
elseif building[INDEX_STORAGE_TANK] then | |||
return building[INDEX_STORAGE_TANK] | |||
end | |||
else | else | ||
return nil | return nil | ||
Line 332: | Line 344: | ||
---getIDsAndRecipesWhereProductID | ---getIDsAndRecipesWhereProductID | ||
---Looks through all the instance's buildings' recipes for any that have the specified product, and returns an array of pairs of building IDs and recipes. | ---Looks through all the instance's buildings' recipes for any that have the specified product, and returns an array of pairs of building IDs and recipes. | ||
--- | |||
---Handles when workshops and gathering huts name their fields differently. | |||
--- | --- | ||
---Benchmarking: ~0.0001 | ---Benchmarking: ~0.0001 | ||
Line 346: | Line 360: | ||
for id, building in pairs(self.dataTable) do | for id, building in pairs(self.dataTable) do | ||
for _, recipe in ipairs(building[INDEX_RECIPES]) do | for _, recipe in ipairs(building[INDEX_RECIPES]) do | ||
if recipe[INDEX_RECIPE_PRODUCT][INDEX_RECIPE_PRODUCT_ID] == productID then | |||
table.insert(ret, { | -- If there's a product subtable with an amount. | ||
["buildingID"] = id, | if recipe[INDEX_RECIPE_PRODUCT] then | ||
if recipe[INDEX_RECIPE_PRODUCT][INDEX_RECIPE_PRODUCT_ID] == productID then | |||
table.insert(ret, { | |||
["buildingID"] = id, | |||
["recipe"] = recipe, | |||
}) | |||
end | |||
-- There may also be seeked deposits. | |||
elseif recipe[INDEX_RECIPE_PRO_DEPOSITS] then | |||
if recipe[INDEX_RECIPE_PRO_DEPOSITS] == productID then | |||
table.insert(ret, { | |||
["buildingID"] = id, | |||
["recipe"] = recipe, | |||
}) | |||
end | |||
-- Or a service provided | |||
elseif recipe[INDEX_RECIPE_PRO_SERVICE] then | |||
if recipe[INDEX_RECIPE_PRO_SERVICE] == productID then | |||
table.insert(ret, { | |||
["buildingID"] = id, | |||
["recipe"] = recipe, | |||
}) | |||
end | |||
end | end | ||
end | end | ||
Line 397: | Line 433: | ||
for id, building in pairs(self.dataTable) do | for id, building in pairs(self.dataTable) do | ||
for _, recipe in ipairs(building[INDEX_RECIPES]) do | for _, recipe in ipairs(building[INDEX_RECIPES]) do | ||
for _, ingredientSlot in ipairs( | |||
local ingredientsList = {} | |||
-- Some recipes don't have ingredients; skip them obviously. | |||
if recipe[INDEX_RECIPE_INGREDIENTS] then | |||
ingredientsList = recipe[INDEX_RECIPE_INGREDIENTS] | |||
elseif recipe[INDEX_RECIPE_ING_SERVICE_GOODS] then | |||
ingredientsList = { recipe[INDEX_RECIPE_ING_SERVICE_GOODS] } | |||
end | |||
for _, ingredientSlot in ipairs(ingredientsList) do | |||
for _, option in ipairs(ingredientSlot) do | for _, option in ipairs(ingredientSlot) do | ||
if option[INDEX_RECIPE_INGREDIENT_OPTION_ID] == ingredientID then | if option[INDEX_RECIPE_INGREDIENT_OPTION_ID] == ingredientID then | ||
Line 405: | Line 450: | ||
}) | }) | ||
break | break | ||
end | |||
end | |||
end | |||
end | |||
end | |||
return ret | |||
end | |||
function BaseDataModel:getIDsAndRecipesWhereProductIDAndIngredientID(productID, ingredientID) | |||
if not self.dataTable then | |||
error(ERROR_MESSAGE_INSTANCE_NOT_INITIALIZED) | |||
end | |||
local ret = {} | |||
for id, building in pairs(self.dataTable) do | |||
for _, recipe in ipairs(building[INDEX_RECIPES]) do | |||
-- If there's a product subtable with an amount. | |||
if recipe[INDEX_RECIPE_PRODUCT] then | |||
if recipe[INDEX_RECIPE_PRODUCT][INDEX_RECIPE_PRODUCT_ID] == productID then | |||
-- Products have ingredients lists. | |||
if recipe[INDEX_RECIPE_INGREDIENTS] then | |||
for _, ingredientSlot in ipairs(recipe[INDEX_RECIPE_INGREDIENTS]) do | |||
for _, option in ipairs(ingredientSlot) do | |||
if option[INDEX_RECIPE_INGREDIENT_OPTION_ID] == ingredientID then | |||
table.insert(ret, { | |||
["buildingID"] = id, | |||
["recipe"] = recipe, | |||
}) | |||
break | |||
end | |||
end | |||
end | |||
end | |||
end | |||
-- There may also be seeked deposits; these never have ingredients, so skip. | |||
elseif recipe[INDEX_RECIPE_PRO_DEPOSITS] then | |||
-- skip, but I want this to have the same structure as other methods. | |||
-- Or a service provided | |||
elseif recipe[INDEX_RECIPE_PRO_SERVICE] then | |||
if recipe[INDEX_RECIPE_PRO_SERVICE] == productID then | |||
-- Services have service goods. | |||
if recipe[INDEX_RECIPE_ING_SERVICE_GOODS] then | |||
for _, ingredientSlot in ipairs(recipe[INDEX_RECIPE_ING_SERVICE_GOODS]) do | |||
for _, option in ipairs(ingredientSlot) do | |||
if option[INDEX_RECIPE_INGREDIENT_OPTION_ID] == ingredientID then | |||
table.insert(ret, { | |||
["buildingID"] = id, | |||
["recipe"] = recipe, | |||
}) | |||
end | |||
end | |||
end | |||
end | end | ||
end | end | ||
Line 430: | Line 532: | ||
local ret = {} | local ret = {} | ||
for _, recipe in ipairs(building[INDEX_RECIPES]) do | for _, recipe in ipairs(building[INDEX_RECIPES]) do | ||
if recipe[INDEX_RECIPE_PRODUCT][INDEX_RECIPE_PRODUCT_ID] == productID then | |||
table.insert(ret, { | -- If there's a product subtable with an amount. | ||
["buildingID"] = buildingID, | if recipe[INDEX_RECIPE_PRODUCT] then | ||
if recipe[INDEX_RECIPE_PRODUCT][INDEX_RECIPE_PRODUCT_ID] == productID then | |||
table.insert(ret, { | |||
["buildingID"] = buildingID, | |||
["recipe"] = recipe, | |||
}) | |||
-- Only one ever per building, so return now | |||
return ret | |||
end | |||
-- There could also be a seeked deposit. | |||
elseif recipe[INDEX_RECIPE_PRO_DEPOSITS] then | |||
if recipe[INDEX_RECIPE_PRO_DEPOSITS] == productID then | |||
table.insert(ret, { | |||
["buildingID"] = buildingID, | |||
["recipe"] = recipe, | |||
}) | |||
-- Only one ever per building, so return now | |||
return ret | |||
end | |||
-- Or a service provided. | |||
elseif recipe[INDEX_RECIPE_PRO_SERVICE] then | |||
if recipe[INDEX_RECIPE_PRO_SERVICE] == productID then | |||
table.insert(ret, { | |||
["buildingID"] = buildingID, | |||
["recipe"] = recipe, | |||
}) | |||
-- Only one ever per building, so return now | |||
return ret | |||
end | |||
end | end | ||
end | end | ||
Line 458: | Line 587: | ||
local ret = {} | local ret = {} | ||
for _, recipe in ipairs(building[INDEX_RECIPES]) do | for _, recipe in ipairs(building[INDEX_RECIPES]) do | ||
for _, ingredientSlot in ipairs( | |||
local ingredientsList = {} | |||
-- Some recipes don't have any ingredients; skip those obviously. | |||
if recipe[INDEX_RECIPE_INGREDIENTS] then | |||
ingredientsList = recipe[INDEX_RECIPE_INGREDIENTS] | |||
elseif recipe[INDEX_RECIPE_ING_SERVICE_GOODS] then | |||
ingredientsList = recipe[INDEX_RECIPE_ING_SERVICE_GOODS] | |||
end | |||
for _, ingredientSlot in ipairs(ingredientsList) do | |||
for _, option in ipairs(ingredientSlot) do | for _, option in ipairs(ingredientSlot) do | ||
if option[INDEX_RECIPE_INGREDIENT_OPTION_ID] == ingredientID then | if option[INDEX_RECIPE_INGREDIENT_OPTION_ID] == ingredientID then | ||
Line 498: | Line 636: | ||
---@return string its productID | ---@return string its productID | ||
function BaseDataModel.getRecipeProductID(recipeData) | function BaseDataModel.getRecipeProductID(recipeData) | ||
return recipeData.recipe[INDEX_RECIPE_PRODUCT][INDEX_RECIPE_PRODUCT_ID] | |||
if recipeData.recipe[INDEX_RECIPE_PRODUCT] then | |||
return recipeData.recipe[INDEX_RECIPE_PRODUCT][INDEX_RECIPE_PRODUCT_ID] | |||
elseif recipeData.recipe[INDEX_RECIPE_PRO_DEPOSITS] then | |||
return recipeData.recipe[INDEX_RECIPE_PRO_DEPOSITS] | |||
elseif recipeData.recipe[INDEX_RECIPE_PRO_SERVICE] then | |||
return recipeData.recipe[INDEX_RECIPE_PRO_SERVICE] | |||
else | |||
return nil | |||
end | |||
end | end | ||
Line 507: | Line 657: | ||
---@return number product amount | ---@return number product amount | ||
function BaseDataModel.getRecipeProductAmount(recipeData) | function BaseDataModel.getRecipeProductAmount(recipeData) | ||
return recipeData.recipe[INDEX_RECIPE_PRODUCT][INDEX_RECIPE_PRODUCT_AMOUNT] | |||
if recipeData.recipe[INDEX_RECIPE_PRODUCT] then | |||
return recipeData.recipe[INDEX_RECIPE_PRODUCT][INDEX_RECIPE_PRODUCT_AMOUNT] | |||
-- Default, for deposits, from which gatherers always bring home 1 and for services, which always provide 1 | |||
else | |||
return 1 | |||
end | |||
end | |||
---isRecipeProvidingService | |||
---Returns true if the provided recipe data table is providing a service | |||
--- | |||
---@param recipeData table a recipe pair retrieved from one of the getters above | |||
---@return boolean true if service, false if product | |||
function BaseDataModel.isRecipeProvidingService(recipeData) | |||
if recipeData.recipe[INDEX_RECIPE_PRO_SERVICE] then | |||
return true | |||
else | |||
return false | |||
end | |||
end | end | ||
Line 516: | Line 686: | ||
---@return number efficiency grade | ---@return number efficiency grade | ||
function BaseDataModel.getRecipeGrade(recipeData) | function BaseDataModel.getRecipeGrade(recipeData) | ||
return CONVERT_GRADE_TO_NUMBER[recipeData.recipe[INDEX_RECIPE_GRADE]] | |||
if recipeData.recipe[INDEX_RECIPE_GRADE] then | |||
return CONVERT_GRADE_TO_NUMBER[recipeData.recipe[INDEX_RECIPE_GRADE]] | |||
elseif recipeData.recipe[INDEX_RECIPE_GRADE_ALT] then | |||
return CONVERT_GRADE_TO_NUMBER[recipeData.recipe[INDEX_RECIPE_GRADE_ALT]] | |||
end | |||
end | end | ||
Line 530: | Line 706: | ||
---getRecipeTime | ---getRecipeTime | ||
---Extracts the production time from the provided recipe data table. | ---Extracts the production time from the provided recipe data table. | ||
--- | |||
---Returns the production time for industry buildings, the sum of planting and harvesting time for farms, and gathering time for camps, etc. | |||
--- | |||
---@param recipeData table a recipe pair retrieved from one of the getters above | ---@param recipeData table a recipe pair retrieved from one of the getters above | ||
---@return number in seconds | ---@return number in seconds | ||
function BaseDataModel.getRecipeTime(recipeData) | function BaseDataModel.getRecipeTime(recipeData) | ||
return recipeData.recipe[ | if recipeData.recipe[INDEX_RECIPE_PRODUCTION_TIME] then | ||
return recipeData.recipe[INDEX_RECIPE_PRODUCTION_TIME] | |||
elseif recipeData.recipe[INDEX_RECIPE_PLANTING_TIME] and recipeData.recipe[INDEX_RECIPE_HARVESTING_TIME] then | |||
return recipeData.recipe[INDEX_RECIPE_PLANTING_TIME] | |||
+ recipeData.recipe[INDEX_RECIPE_HARVESTING_TIME] | |||
elseif recipeData.recipe[INDEX_RECIPE_GATHERING_TIME] then | |||
return recipeData.recipe[INDEX_RECIPE_GATHERING_TIME] | |||
else -- for example, services don't have a time | |||
return 0 | |||
end | |||
end | end | ||
---getRecipeNumIngredientSlots | ---getRecipeNumIngredientSlots | ||
---Counts the number of slots for ingredients found in the provided recipe data table. | ---Counts the number of slots for ingredients found in the provided recipe data table. | ||
--- | |||
---If the recipe has no ingredients (like for farms and camps), this safely returns zero. | |||
--- | |||
---@param recipeData table a recipe pair retrieved from one of the getters above | ---@param recipeData table a recipe pair retrieved from one of the getters above | ||
---@return number of ingredients (slots of options) | ---@return number of ingredients (slots of options) | ||
function BaseDataModel.getRecipeNumIngredientSlots(recipeData) | function BaseDataModel.getRecipeNumIngredientSlots(recipeData) | ||
return #recipeData.recipe[INDEX_RECIPE_INGREDIENTS] | |||
if recipeData.recipe[INDEX_RECIPE_INGREDIENTS] then | |||
return #recipeData.recipe[INDEX_RECIPE_INGREDIENTS] | |||
elseif recipeData.recipe[INDEX_RECIPE_ING_SERVICE_GOODS] then | |||
return #recipeData.recipe[INDEX_RECIPE_ING_SERVICE_GOODS] | |||
--Camps, for example, don't have ingredients. | |||
else | |||
return 0 | |||
end | |||
end | end | ||
---getRecipeIngredientNumOptions | ---getRecipeIngredientNumOptions | ||
---Counts the number of options for the specified ingredient slot in the provided recipe data table. | ---Counts the number of options for the specified ingredient slot in the provided recipe data table. | ||
--- | |||
---This should never be called if the ingredients table doesn't exist. | |||
--- | |||
---@param recipeData table a recipe pair retrieved from one of the getters above | ---@param recipeData table a recipe pair retrieved from one of the getters above | ||
---@param i number index of which ingredient slot | ---@param i number index of which ingredient slot | ||
---@return number of options for that ingredient slot | ---@return number of options for that ingredient slot | ||
function BaseDataModel.getRecipeIngredientNumOptions(recipeData, i) | function BaseDataModel.getRecipeIngredientNumOptions(recipeData, i) | ||
return #recipeData.recipe[INDEX_RECIPE_INGREDIENTS][i] | |||
if recipeData.recipe[INDEX_RECIPE_INGREDIENTS] then | |||
return #recipeData.recipe[INDEX_RECIPE_INGREDIENTS][i] | |||
elseif recipeData.recipe[INDEX_RECIPE_ING_SERVICE_GOODS] then | |||
-- Should always be 1, but let's not assume | |||
return #recipeData.recipe[INDEX_RECIPE_ING_SERVICE_GOODS] | |||
else -- Any other cases have no options at all. | |||
return 0 | |||
end | |||
end | end | ||
---getRecipeIngredientOptionIDAt | ---getRecipeIngredientOptionIDAt | ||
---Extracts the good ID for the specified option for the specified ingredient slot in the provided recipe data table. | ---Extracts the good ID for the specified option for the specified ingredient slot in the provided recipe data table. | ||
--- | |||
---This should never be called if the ingredients table doesn't exist. | |||
--- | --- | ||
---@param recipeData table a recipe pair retrieved from one of the getters above | ---@param recipeData table a recipe pair retrieved from one of the getters above | ||
Line 564: | Line 780: | ||
---@return string good ID | ---@return string good ID | ||
function BaseDataModel.getRecipeIngredientOptionIDAt(recipeData, i, j) | function BaseDataModel.getRecipeIngredientOptionIDAt(recipeData, i, j) | ||
local optionsList = recipeData.recipe[INDEX_RECIPE_INGREDIENTS][i] | |||
if recipeData.recipe[INDEX_RECIPE_INGREDIENTS] then | |||
local optionsList = recipeData.recipe[INDEX_RECIPE_INGREDIENTS][i] | |||
return optionsList[j][INDEX_RECIPE_INGREDIENT_OPTION_ID] | |||
elseif recipeData.recipe[INDEX_RECIPE_ING_SERVICE_GOODS] then | |||
return recipeData.recipe[INDEX_RECIPE_ING_SERVICE_GOODS][i][INDEX_RECIPE_INGREDIENT_OPTION_ID] | |||
end | |||
end | end | ||
---getRecipeIngredientOptionAmountAt | ---getRecipeIngredientOptionAmountAt | ||
---Extracts the good ID for the specified option for the specified ingredient slot in the provided recipe data table. | ---Extracts the good ID for the specified option for the specified ingredient slot in the provided recipe data table. | ||
--- | |||
---This should never be called if the ingredients table doesn't exist. | |||
--- | --- | ||
---@param recipeData table a recipe pair retrieved from one of the getters above | ---@param recipeData table a recipe pair retrieved from one of the getters above | ||
---@param i number index of which ingredient slot | ---@param i number index of which ingredient slot | ||
---@param j table index of which option at that slot | ---@param j table index of which option at that slot | ||
---@return number | ---@return number ingredient amount | ||
function BaseDataModel.getRecipeIngredientOptionAmountAt(recipeData, i, j) | function BaseDataModel.getRecipeIngredientOptionAmountAt(recipeData, i, j) | ||
local optionsList = recipeData.recipe[INDEX_RECIPE_INGREDIENTS][i] | |||
if recipeData.recipe[INDEX_RECIPE_INGREDIENTS] then | |||
local optionsList = recipeData.recipe[INDEX_RECIPE_INGREDIENTS][i] | |||
return optionsList[j][INDEX_RECIPE_INGREDIENT_OPTION_AMOUNT] | |||
elseif recipeData.recipe[INDEX_RECIPE_ING_SERVICE_GOODS] then | |||
return recipeData.recipe[INDEX_RECIPE_ING_SERVICE_GOODS][i][INDEX_RECIPE_INGREDIENT_OPTION_AMOUNT] | |||
end | |||
end | end | ||
Latest revision as of 20:57, 31 October 2024
Documentation for this module may be created at Module:BaseDataModel/doc
--- @module BaseDataModel local BaseDataModel = {} --region Dependencies local JsonUtils = require("Module:JsonUtils") --endregion --region Private constants local INDEX_CATEGORY = "category" local INDEX_CITY_SCORE = "cityScore" local INDEX_CONSTRUCTION_TIME = "constructionTime" local INDEX_DESCRIPTION = "description" local INDEX_NAME = "displayName" local INDEX_ID = "id" local INDEX_IS_ESSENTIAL = "initiallyEssential" local INDEX_IS_MOVABLE = "movable" local INDEX_RECIPES = "recipes" local INDEX_RECIPE_GRADE = "grade" local INDEX_RECIPE_GRADE_ALT = "gradeId" local INDEX_RECIPE_INGREDIENTS = "ingredients" local INDEX_RECIPE_INGREDIENT_OPTION_AMOUNT = "amount" local INDEX_RECIPE_INGREDIENT_OPTION_ID = "name" local INDEX_RECIPE_ING_SERVICE_GOODS = "goods" -- alt to ingredients for service buildings local INDEX_RECIPE_PRODUCT = "product" local INDEX_RECIPE_PRODUCT_AMOUNT = "amount" local INDEX_RECIPE_PRODUCT_ID = "name" local INDEX_RECIPE_PRO_DEPOSITS = "seekedDeposits" -- alt to product for huts and camps local INDEX_RECIPE_PRO_SERVICE = "servedNeed" -- alt to product for service buildings local INDEX_RECIPE_PRODUCTION_TIME = "productionTime" local INDEX_RECIPE_PLANTING_TIME = "plantingTime" local INDEX_RECIPE_HARVESTING_TIME = "harvestingTime" local INDEX_RECIPE_GATHERING_TIME = "gatheringTime" local INDEX_CONSTRUCTION_GOODS = "requiredGoods" local INDEX_CONSTRUCTION_GOODS_AMOUNT = "amount" local INDEX_CONSTRUCTION_GOODS_ID = "name" local INDEX_SIZE_X = "sizeX" local INDEX_SIZE_Y = "sizeY" local INDEX_STORAGE_CAP = "storage" local INDEX_STORAGE_TANK = "baseTankCapacity" local INDEX_WORKPLACES = "workplaces" local CONVERT_GRADE_TO_NUMBER = { ["Grade0"] = 0, ["Grade1"] = 1, ["Grade2"] = 2, ["Grade3"] = 3, } local ERROR_MESSAGE_INSTANCE_NOT_INITIALIZED = "This instance's data table was never initialized. Please check how and where this method was called to diagnose the problem" --endregion --region Private member variables --- Main data array, indexed by ID. local dataTable --- Lookup table to make searching by names instant. local mapNamesToIDs --endregion --region Private methods ---load ---Loads the JSON data into the two structured member variables for data access. --- ---Benchmarking: ~0.015 seconds --- ---@param instance table an instance of BaseDataModel ---@param dataFile string filename of Json data to load into this instance ---@return table, table instance's data structures --when uncommented for debugging local function load(instance, dataFile) -- Utility module loads the data from JSON. local rawTable = JsonUtils.convertJSONToLuaTable(dataFile) -- Lightweight (no deep copying) restructuring for faster look-ups. instance.dataTable = {} instance.mapNamesToIDs = {} for _, record in ipairs(rawTable) do local key = record[INDEX_ID] local name = record[INDEX_NAME] instance.dataTable[key] = record instance.mapNamesToIDs[name] = key end --return instance.dataTable, instance.mapNamesToIDs --uncomment when debugging end ---findID ---called with self and an ID, gets the record with that ID from that instance ---@param instance table an instance of BaseDataModel ---@param id string the ID ---@return table the record with that ID from that instance, or nil if not found local function findID(instance, id) if not instance.dataTable then error(ERROR_MESSAGE_INSTANCE_NOT_INITIALIZED) end return instance.dataTable[id] end ---findName ---called with self and a name gets the record with that name from that instance ---@param instance table an instance of BaseDataModel ---@param name string the display name ---@return table the record with that name, or nil if not found local function findName(instance, name) if not instance.dataTable or not instance.mapNamesToIDs then error(ERROR_MESSAGE_INSTANCE_NOT_INITIALIZED) end local id = instance.mapNamesToIDs[name] if not id then return nil end return instance.dataTable[id] end --endregion --region Public building interface -- All building data models implement this interface -- getID(displayName) -- getCategory(id) -- getCityScore(id) -- getConstructionCosts(id) -- returns { [goodName] = stack size } -- getConstructionTime(id) -- getDescription(id) -- getIcon(id) -- isMovable(id) -- getName(id) -- getNumberOfWorkplaces(id) -- getSize(id) -- returns string "X x Y" -- getStorage(id) ---getID finds the specified name in the instance's data and returns the associated ID ---@param name string the display name ---@return string the ID, or nil if not found function BaseDataModel:getID(name) local building = findName(self, name) if building then return building[INDEX_ID] else return nil end end ---getCategory from the instance's data using an ID ---@param id string the ID ---@return string the category, or nil if not found function BaseDataModel:getCategory(id) local building = findID(self, id) if building then return building[INDEX_CATEGORY] else return nil end end ---getCityScore from the instance's data using an ID ---@param id string the ID ---@return number the city score, or nil if not found function BaseDataModel:getCityScore(id) local building = findID(self, id) if building then return building[INDEX_CITY_SCORE] else return nil end end ---getConstructionCosts from the instance's data using an ID in the form of an array of short tables describing goods required for construction. --- example = { --- [1] = { --- ["amount"] = 5, --- ["name"] = "[Mat Processed] Planks", -- note that this is NOT a name, but an ID --- }, --- [2] = { --- ["amount"] = 2, --- ["name"] = "[Mat Processed] Bricks", -- note that this is NOT a name, but an ID --- }, --- } ---@param id string the ID ---@return table an array of tables with amount and ID function BaseDataModel:getConstructionCosts(id) local building = findID(self, id) if building then return building[INDEX_CONSTRUCTION_GOODS] else return nil end end ---getConstructionTime from the instance's data using an ID ---@param id string the ID ---@return number the number of seconds, or nil if not found function BaseDataModel:getConstructionTime(id) local building = findID(self, id) if building then return building[INDEX_CONSTRUCTION_TIME] else return nil end end ---getDescription from the instance's data using an ID ---@param id string the ID ---@return string the description including sprite markup, or nil if not found function BaseDataModel:getDescription(id) local building = findID(self, id) if building then return building[INDEX_DESCRIPTION] else return nil end end ---getIcon from the instance's data using an ID ---@param id string the ID ---@return string the icon filename, or nil if not found function BaseDataModel:getIcon(id) local building = findID(self, id) if building then return id .. "_icon.png" else return nil end end ---isMovable from the instance's data using an ID ---@param id string the ID ---@return boolean true if the building can be moved, false if not (or nil if not found) function BaseDataModel:isMovable(id) local building = findID(self, id) if building then return building[INDEX_IS_MOVABLE] else return nil end end ---getName from the instance's data using an ID ---@param id string the ID ---@return string the name, or nil if not found function BaseDataModel:getName(id) local building = findID(self, id) if building then return building[INDEX_NAME] else return nil end end ---getNumberOfWorkplaces from the instance's data using an ID ---@param id string the ID ---@return number how many worker slots, or nil if not found function BaseDataModel:getNumberOfWorkplaces(id) local building = findID(self, id) if building then return building[INDEX_WORKPLACES] else return nil end end ---getSize from the instance's data using an ID ---@param id string the ID ---@return string as x-by-y, or nil if not found function BaseDataModel:getSize(id) local building = findID(self, id) if building then return building[INDEX_SIZE_X] .. " × " .. building[INDEX_SIZE_Y] else return nil end end ---getStorage from the instance's data using an ID ---@param id string the ID ---@return number the storage capacity, or nil if not found function BaseDataModel:getStorage(id) local building = findID(self, id) if building then if building[INDEX_STORAGE_CAP] then return building[INDEX_STORAGE_CAP] elseif building[INDEX_STORAGE_TANK] then return building[INDEX_STORAGE_TANK] end else return nil end end --endregion --region Public building recipe query interface -- All methods in the Building recipe query interface returns an array of pairs of building IDs and recipes, pulled straight from the JSON-based data table: -- [1] = { -- [1].buildingID = string --building ID -- [1].recipe = { -- ["product"] = { --product info -- ["name"] = string --product ID *NOT* name! -- ["amount"] = number -- } -- ["grade"] = string --like "Grade2" -- ["productionTime"] = number --in seconds -- ["ingredients"] = { --ingredient slots (between 0-3) -- [1] = { --option list (between 1-6) -- [1] = { --option info -- ["name"] = string --ingredient ID *NOT* name -- ["amount"] = number -- }, -- ... --next option -- }, -- ...--next ingredient slot -- } -- } -- } -- getIDsAndRecipesWhereProductID(productID) -- getIDsAndRecipesWhereBuildingID(buildingID) -- getIDsAndRecipesWhereIngredientID(ingredientID) -- getIDsAndRecipesWhereProductIDAndBuildingID(productID, buildingID) -- getIDsAndRecipesWhereIngredientIDAndBuildingID(ingredientID, buildingID) ---getIDsAndRecipesWhereProductID ---Looks through all the instance's buildings' recipes for any that have the specified product, and returns an array of pairs of building IDs and recipes. --- ---Handles when workshops and gathering huts name their fields differently. --- ---Benchmarking: ~0.0001 --- ---@param productID string the product ---@return table array of pairs of buildingIDs and recipes, or {} if none found function BaseDataModel:getIDsAndRecipesWhereProductID(productID) if not self.dataTable then error(ERROR_MESSAGE_INSTANCE_NOT_INITIALIZED) end local ret = {} for id, building in pairs(self.dataTable) do for _, recipe in ipairs(building[INDEX_RECIPES]) do -- If there's a product subtable with an amount. if recipe[INDEX_RECIPE_PRODUCT] then if recipe[INDEX_RECIPE_PRODUCT][INDEX_RECIPE_PRODUCT_ID] == productID then table.insert(ret, { ["buildingID"] = id, ["recipe"] = recipe, }) end -- There may also be seeked deposits. elseif recipe[INDEX_RECIPE_PRO_DEPOSITS] then if recipe[INDEX_RECIPE_PRO_DEPOSITS] == productID then table.insert(ret, { ["buildingID"] = id, ["recipe"] = recipe, }) end -- Or a service provided elseif recipe[INDEX_RECIPE_PRO_SERVICE] then if recipe[INDEX_RECIPE_PRO_SERVICE] == productID then table.insert(ret, { ["buildingID"] = id, ["recipe"] = recipe, }) end end end end return ret end ---getIDsAndRecipesWhereBuildingID ---Loads the instance's building's recipes into an array of pairs of the same building ID and those recipes. --- ---Benchmarking: ~0.0000 seconds --- ---@param buildingID string the building ---@return table array of pairs of buildingIDs and recipes, or {} if none found function BaseDataModel:getIDsAndRecipesWhereBuildingID(buildingID) local building = findID(self, buildingID) if not building then return {} end local ret = {} for _, recipe in ipairs(building[INDEX_RECIPES]) do table.insert(ret, { ["buildingID"] = buildingID, ["recipe"] = recipe, }) end return ret end ---getIDsAndRecipesWhereIngredientID ---Looks through all the instance's buildings' recipes to find any with the specified ingredients, and returns an array of pairs of building IDs and recipes. --- ---Benchmarking: ~0.0003 seconds --- ---@param ingredientID string the ingredient ---@return table array of pairs of buildingIDs and recipes, or {} if none found function BaseDataModel:getIDsAndRecipesWhereIngredientID(ingredientID) if not self.dataTable then error(ERROR_MESSAGE_INSTANCE_NOT_INITIALIZED) end local ret = {} for id, building in pairs(self.dataTable) do for _, recipe in ipairs(building[INDEX_RECIPES]) do local ingredientsList = {} -- Some recipes don't have ingredients; skip them obviously. if recipe[INDEX_RECIPE_INGREDIENTS] then ingredientsList = recipe[INDEX_RECIPE_INGREDIENTS] elseif recipe[INDEX_RECIPE_ING_SERVICE_GOODS] then ingredientsList = { recipe[INDEX_RECIPE_ING_SERVICE_GOODS] } end for _, ingredientSlot in ipairs(ingredientsList) do for _, option in ipairs(ingredientSlot) do if option[INDEX_RECIPE_INGREDIENT_OPTION_ID] == ingredientID then table.insert(ret, { ["buildingID"] = id, ["recipe"] = recipe, }) break end end end end end return ret end function BaseDataModel:getIDsAndRecipesWhereProductIDAndIngredientID(productID, ingredientID) if not self.dataTable then error(ERROR_MESSAGE_INSTANCE_NOT_INITIALIZED) end local ret = {} for id, building in pairs(self.dataTable) do for _, recipe in ipairs(building[INDEX_RECIPES]) do -- If there's a product subtable with an amount. if recipe[INDEX_RECIPE_PRODUCT] then if recipe[INDEX_RECIPE_PRODUCT][INDEX_RECIPE_PRODUCT_ID] == productID then -- Products have ingredients lists. if recipe[INDEX_RECIPE_INGREDIENTS] then for _, ingredientSlot in ipairs(recipe[INDEX_RECIPE_INGREDIENTS]) do for _, option in ipairs(ingredientSlot) do if option[INDEX_RECIPE_INGREDIENT_OPTION_ID] == ingredientID then table.insert(ret, { ["buildingID"] = id, ["recipe"] = recipe, }) break end end end end end -- There may also be seeked deposits; these never have ingredients, so skip. elseif recipe[INDEX_RECIPE_PRO_DEPOSITS] then -- skip, but I want this to have the same structure as other methods. -- Or a service provided elseif recipe[INDEX_RECIPE_PRO_SERVICE] then if recipe[INDEX_RECIPE_PRO_SERVICE] == productID then -- Services have service goods. if recipe[INDEX_RECIPE_ING_SERVICE_GOODS] then for _, ingredientSlot in ipairs(recipe[INDEX_RECIPE_ING_SERVICE_GOODS]) do for _, option in ipairs(ingredientSlot) do if option[INDEX_RECIPE_INGREDIENT_OPTION_ID] == ingredientID then table.insert(ret, { ["buildingID"] = id, ["recipe"] = recipe, }) end end end end end end end end return ret end ---getIDsAndRecipesWhereProductIDAndBuildingID ---Looks through the instance's specified buildings' recipes to see whether one has the specified product, and returns an array of pairs of building IDs and recipes. --- ---Benchmarking: ~0.0000 seconds --- ---@param productID string the product ---@param buildingID string the building ---@return table array of pairs of buildingIDs and recipes, or {} if none found function BaseDataModel:getIDsAndRecipesWhereProductIDAndBuildingID(productID, buildingID) local building = findID(self, buildingID) if not building then return {} end local ret = {} for _, recipe in ipairs(building[INDEX_RECIPES]) do -- If there's a product subtable with an amount. if recipe[INDEX_RECIPE_PRODUCT] then if recipe[INDEX_RECIPE_PRODUCT][INDEX_RECIPE_PRODUCT_ID] == productID then table.insert(ret, { ["buildingID"] = buildingID, ["recipe"] = recipe, }) -- Only one ever per building, so return now return ret end -- There could also be a seeked deposit. elseif recipe[INDEX_RECIPE_PRO_DEPOSITS] then if recipe[INDEX_RECIPE_PRO_DEPOSITS] == productID then table.insert(ret, { ["buildingID"] = buildingID, ["recipe"] = recipe, }) -- Only one ever per building, so return now return ret end -- Or a service provided. elseif recipe[INDEX_RECIPE_PRO_SERVICE] then if recipe[INDEX_RECIPE_PRO_SERVICE] == productID then table.insert(ret, { ["buildingID"] = buildingID, ["recipe"] = recipe, }) -- Only one ever per building, so return now return ret end end end return {} end ---getIDsAndRecipesWhereIngredientIDAndBuildingID ---Looks through the instance's specified building's recipes to see whether any have the specified ingredient, and returns an array of pairs of building IDs and recipes. --- ---Benchmarking: ~0.0000 seconds --- ---@param ingredientID string ---@param buildingID string ---@return table array of pairs of buildingIDs and recipes, or {} if none found function BaseDataModel:getIDsAndRecipesWhereIngredientIDAndBuildingID(ingredientID, buildingID) local building = findID(self, buildingID) if not building then return {} end local ret = {} for _, recipe in ipairs(building[INDEX_RECIPES]) do local ingredientsList = {} -- Some recipes don't have any ingredients; skip those obviously. if recipe[INDEX_RECIPE_INGREDIENTS] then ingredientsList = recipe[INDEX_RECIPE_INGREDIENTS] elseif recipe[INDEX_RECIPE_ING_SERVICE_GOODS] then ingredientsList = recipe[INDEX_RECIPE_ING_SERVICE_GOODS] end for _, ingredientSlot in ipairs(ingredientsList) do for _, option in ipairs(ingredientSlot) do if option[INDEX_RECIPE_INGREDIENT_OPTION_ID] == ingredientID then table.insert(ret, { ["buildingID"] = buildingID, ["recipe"] = recipe, }) break end end end end return ret end --endregion --region Public recipe data retrieval interface -- Methods in this interface take a recipe taken from this BaseDataModel and return specific information within. This module owns the access to the inner data. -- Unlike the recipe query and building interfaces, these methods are NOT called on instances, but the static class name, BaseDataModel. -- -- BaseDataModel.getRecipeProductID(recipeData) -- BaseDataModel.getRecipeProductAmount(recipeData) -- BaseDataModel.getRecipeGrade(recipeData) -- BaseDataModel.getRecipeBuildingID(recipeData) -- BaseDataModel.getRecipeTime(recipeData) -- BaseDataModel.getRecipeNumIngredientSlots(recipeData) -- BaseDataModel.getRecipeIngredientNumOptions(recipeData, i) -- BaseDataModel.getRecipeIngredientOptionIDAt(recipeData, i, j) -- BaseDataModel.getRecipeIngredientOptionAmountAt(recipeData, i, j) ---getRecipeProductID ---Extracts the product ID from the provided recipe data table. --- ---@param recipeData table a recipe pair retrieved from one of the getters above ---@return string its productID function BaseDataModel.getRecipeProductID(recipeData) if recipeData.recipe[INDEX_RECIPE_PRODUCT] then return recipeData.recipe[INDEX_RECIPE_PRODUCT][INDEX_RECIPE_PRODUCT_ID] elseif recipeData.recipe[INDEX_RECIPE_PRO_DEPOSITS] then return recipeData.recipe[INDEX_RECIPE_PRO_DEPOSITS] elseif recipeData.recipe[INDEX_RECIPE_PRO_SERVICE] then return recipeData.recipe[INDEX_RECIPE_PRO_SERVICE] else return nil end end ---getRecipeProductAmount ---Extracts the product amount from the provided recipe data table. ---@param recipeData table a recipe pair retrieved from one of the getters above ---@return number product amount function BaseDataModel.getRecipeProductAmount(recipeData) if recipeData.recipe[INDEX_RECIPE_PRODUCT] then return recipeData.recipe[INDEX_RECIPE_PRODUCT][INDEX_RECIPE_PRODUCT_AMOUNT] -- Default, for deposits, from which gatherers always bring home 1 and for services, which always provide 1 else return 1 end end ---isRecipeProvidingService ---Returns true if the provided recipe data table is providing a service --- ---@param recipeData table a recipe pair retrieved from one of the getters above ---@return boolean true if service, false if product function BaseDataModel.isRecipeProvidingService(recipeData) if recipeData.recipe[INDEX_RECIPE_PRO_SERVICE] then return true else return false end end ---getRecipeGrade ---Extracts the efficiency grade from the provided recipe data table. ---@param recipeData table a recipe pair retrieved from one of the getters above ---@return number efficiency grade function BaseDataModel.getRecipeGrade(recipeData) if recipeData.recipe[INDEX_RECIPE_GRADE] then return CONVERT_GRADE_TO_NUMBER[recipeData.recipe[INDEX_RECIPE_GRADE]] elseif recipeData.recipe[INDEX_RECIPE_GRADE_ALT] then return CONVERT_GRADE_TO_NUMBER[recipeData.recipe[INDEX_RECIPE_GRADE_ALT]] end end ---getRecipeBuildingID ---Extracts the building ID from the provided recipe data table where this recipe was found. ---@param recipeData table a recipe pair retrieved from one of the getters above ---@return string ID of the building that makes this recipe function BaseDataModel.getRecipeBuildingID(recipeData) return recipeData.buildingID end ---getRecipeTime ---Extracts the production time from the provided recipe data table. --- ---Returns the production time for industry buildings, the sum of planting and harvesting time for farms, and gathering time for camps, etc. --- ---@param recipeData table a recipe pair retrieved from one of the getters above ---@return number in seconds function BaseDataModel.getRecipeTime(recipeData) if recipeData.recipe[INDEX_RECIPE_PRODUCTION_TIME] then return recipeData.recipe[INDEX_RECIPE_PRODUCTION_TIME] elseif recipeData.recipe[INDEX_RECIPE_PLANTING_TIME] and recipeData.recipe[INDEX_RECIPE_HARVESTING_TIME] then return recipeData.recipe[INDEX_RECIPE_PLANTING_TIME] + recipeData.recipe[INDEX_RECIPE_HARVESTING_TIME] elseif recipeData.recipe[INDEX_RECIPE_GATHERING_TIME] then return recipeData.recipe[INDEX_RECIPE_GATHERING_TIME] else -- for example, services don't have a time return 0 end end ---getRecipeNumIngredientSlots ---Counts the number of slots for ingredients found in the provided recipe data table. --- ---If the recipe has no ingredients (like for farms and camps), this safely returns zero. --- ---@param recipeData table a recipe pair retrieved from one of the getters above ---@return number of ingredients (slots of options) function BaseDataModel.getRecipeNumIngredientSlots(recipeData) if recipeData.recipe[INDEX_RECIPE_INGREDIENTS] then return #recipeData.recipe[INDEX_RECIPE_INGREDIENTS] elseif recipeData.recipe[INDEX_RECIPE_ING_SERVICE_GOODS] then return #recipeData.recipe[INDEX_RECIPE_ING_SERVICE_GOODS] --Camps, for example, don't have ingredients. else return 0 end end ---getRecipeIngredientNumOptions ---Counts the number of options for the specified ingredient slot in the provided recipe data table. --- ---This should never be called if the ingredients table doesn't exist. --- ---@param recipeData table a recipe pair retrieved from one of the getters above ---@param i number index of which ingredient slot ---@return number of options for that ingredient slot function BaseDataModel.getRecipeIngredientNumOptions(recipeData, i) if recipeData.recipe[INDEX_RECIPE_INGREDIENTS] then return #recipeData.recipe[INDEX_RECIPE_INGREDIENTS][i] elseif recipeData.recipe[INDEX_RECIPE_ING_SERVICE_GOODS] then -- Should always be 1, but let's not assume return #recipeData.recipe[INDEX_RECIPE_ING_SERVICE_GOODS] else -- Any other cases have no options at all. return 0 end end ---getRecipeIngredientOptionIDAt ---Extracts the good ID for the specified option for the specified ingredient slot in the provided recipe data table. --- ---This should never be called if the ingredients table doesn't exist. --- ---@param recipeData table a recipe pair retrieved from one of the getters above ---@param i number index of which ingredient slot ---@param j table index of which option at that slot ---@return string good ID function BaseDataModel.getRecipeIngredientOptionIDAt(recipeData, i, j) if recipeData.recipe[INDEX_RECIPE_INGREDIENTS] then local optionsList = recipeData.recipe[INDEX_RECIPE_INGREDIENTS][i] return optionsList[j][INDEX_RECIPE_INGREDIENT_OPTION_ID] elseif recipeData.recipe[INDEX_RECIPE_ING_SERVICE_GOODS] then return recipeData.recipe[INDEX_RECIPE_ING_SERVICE_GOODS][i][INDEX_RECIPE_INGREDIENT_OPTION_ID] end end ---getRecipeIngredientOptionAmountAt ---Extracts the good ID for the specified option for the specified ingredient slot in the provided recipe data table. --- ---This should never be called if the ingredients table doesn't exist. --- ---@param recipeData table a recipe pair retrieved from one of the getters above ---@param i number index of which ingredient slot ---@param j table index of which option at that slot ---@return number ingredient amount function BaseDataModel.getRecipeIngredientOptionAmountAt(recipeData, i, j) if recipeData.recipe[INDEX_RECIPE_INGREDIENTS] then local optionsList = recipeData.recipe[INDEX_RECIPE_INGREDIENTS][i] return optionsList[j][INDEX_RECIPE_INGREDIENT_OPTION_AMOUNT] elseif recipeData.recipe[INDEX_RECIPE_ING_SERVICE_GOODS] then return recipeData.recipe[INDEX_RECIPE_ING_SERVICE_GOODS][i][INDEX_RECIPE_INGREDIENT_OPTION_AMOUNT] end end --endregion --region Public constructor ---new ---Constructs a new BaseDataModel by loading the specified data file into the instance. --- ---Benchmarking: ~0.0151 seconds --- ---@param dataFile string filename of Json data to load into this instance function BaseDataModel.new(dataFile) local instance = {} setmetatable(instance, { __index = BaseDataModel }) load(instance, dataFile) return instance end --endregion return BaseDataModel