Module:BaseDataModel: Difference between revisions
From Against the Storm Official Wiki
m (Prepping for better module inheritance and robustness. no functional changes yet) |
m (Prepping for better module inheritance and robustness. no functional changes yet) |
||
Line 802: | Line 802: | ||
--region Public recipe data retrieval interface | --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. | -- 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. | ||
Line 827: | Line 821: | ||
---@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 string its productID | ---@return string its productID | ||
function BaseDataModel | function BaseDataModel:getRecipeProductID(recipeData) | ||
if recipeData.recipe[INDEX_RECIPE_PRODUCT] then | if recipeData.recipe[INDEX_RECIPE_PRODUCT] then | ||
Line 848: | Line 842: | ||
---@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 product amount | ---@return number product amount | ||
function BaseDataModel | function BaseDataModel:getRecipeProductAmount(recipeData) | ||
if recipeData.recipe[INDEX_RECIPE_PRODUCT] then | if recipeData.recipe[INDEX_RECIPE_PRODUCT] then | ||
Line 864: | Line 858: | ||
---@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 boolean true if service, false if product | ---@return boolean true if service, false if product | ||
function BaseDataModel | function BaseDataModel:isRecipeProvidingService(recipeData) | ||
if recipeData.recipe[INDEX_RECIPE_PRO_SERVICE] then | if recipeData.recipe[INDEX_RECIPE_PRO_SERVICE] then | ||
return true | return true | ||
Line 877: | Line 871: | ||
---@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 efficiency grade | ---@return number efficiency grade | ||
function BaseDataModel | function BaseDataModel:getRecipeGrade(recipeData) | ||
if recipeData.recipe[INDEX_RECIPE_GRADE] then | if recipeData.recipe[INDEX_RECIPE_GRADE] then | ||
Line 895: | Line 889: | ||
---@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 string ID of the building that makes this recipe | ---@return string ID of the building that makes this recipe | ||
function BaseDataModel | function BaseDataModel:getRecipeBuildingID(recipeData) | ||
return recipeData.buildingID | return recipeData.buildingID | ||
end | end | ||
Line 906: | Line 900: | ||
---@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 | function BaseDataModel:getRecipeTime(recipeData) | ||
if recipeData.recipe[INDEX_RECIPE_PRODUCTION_TIME] then | if recipeData.recipe[INDEX_RECIPE_PRODUCTION_TIME] then | ||
return recipeData.recipe[INDEX_RECIPE_PRODUCTION_TIME] | return recipeData.recipe[INDEX_RECIPE_PRODUCTION_TIME] | ||
Line 929: | Line 923: | ||
---@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 | function BaseDataModel:getRecipeNumIngredientSlots(recipeData) | ||
if recipeData.recipe[INDEX_RECIPE_INGREDIENTS] then | if recipeData.recipe[INDEX_RECIPE_INGREDIENTS] then | ||
Line 951: | Line 945: | ||
---@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 | function BaseDataModel:getRecipeIngredientNumOptions(recipeData, i) | ||
if recipeData.recipe[INDEX_RECIPE_INGREDIENTS] then | if recipeData.recipe[INDEX_RECIPE_INGREDIENTS] then | ||
Line 974: | Line 968: | ||
---@param j table index of which option at that slot | ---@param j table index of which option at that slot | ||
---@return string good ID | ---@return string good ID | ||
function BaseDataModel | function BaseDataModel:getRecipeIngredientOptionIDAt(recipeData, i, j) | ||
if recipeData.recipe[INDEX_RECIPE_INGREDIENTS] then | if recipeData.recipe[INDEX_RECIPE_INGREDIENTS] then | ||
Line 994: | Line 988: | ||
---@param j table index of which option at that slot | ---@param j table index of which option at that slot | ||
---@return number ingredient amount | ---@return number ingredient amount | ||
function BaseDataModel | function BaseDataModel:getRecipeIngredientOptionAmountAt(recipeData, i, j) | ||
if recipeData.recipe[INDEX_RECIPE_INGREDIENTS] then | if recipeData.recipe[INDEX_RECIPE_INGREDIENTS] then |
Revision as of 19:08, 5 November 2024
Documentation for this module may be created at Module:BaseDataModel/doc
---@class BaseDataModel --- ---@alias buildingIDString string the unique identifier for a building ---@alias buildingRecordTable table a full record for one building ---@alias recipeTable table a record for one recipe ---@alias constructionGoodsTable table an array of construction goods' IDs and amounts --- ---Provides a standard set of getters for building data and permits overrides from derived classes to specialize the building data, to allow for exceptions, etc. --- ---@field getID function interface method ---@field getCategory function interface method ---@field getCityScore function interface method ---@field getConstructionCosts function interface method ---@field getConstructionTime function interface method ---@field getDescription function interface method ---@field getIcon function interface method ---@field isMovable function interface method ---@field getName function interface method ---@field getNumberOfWorkplaces function interface method ---@field getSize function interface method ---@field getStorage function interface method ---@field isServiceBuilding function interface method --- ---@field _ --- --- --- --- ---@field dataTable table Primary data table of _buildingRecordTables_, keyed by _buildingIDString_ ---@field mapNamesToIDs table Lookup table, returns a _buildingIDString_ from a building's display name ---@field schema table data schema, with field names and subtables of field names local BaseDataModel = {} --region Dependencies local JsonUtils = require("Module:JsonUtils") --endregion --region Protected data schema ---@protected ---Protected data schema defining Json field names for accessing data. ---@field schema table data schema, with field names and subtables of field names BaseDataModel.schema = { ID = "id", NAME = "displayName", CATEGORY1 = "category", CITY_SCORE = "cityScore", CONSTRUCTION_TIME = "constructionTime", CONSTRUCTION_GOODS = "requiredGoods", CONSTRUCTION = { ID = "name", AMOUNT = "amount", }, DESCRIPTION = "description", IS_ESSENTIAL = "initiallyEssential", IS_MOVABLE = "movable", SIZE_X = "sizeX", SIZE_Y = "sizeY", STORAGE_CAP = "storage", WORKPLACES = "workplaces", RECIPE = { GRADE = "grade", PRODUCTION_TIME = "productionTime", PRODUCTS = "product", PRODUCT = { ID = "name", AMOUNT = "amount", }, INGREDIENTS = "ingredients", INGREDIENT = { OPTION = { ID = "name", AMOUNT = "amount", }, }, }, } ---Secondary category, to be defined by derived classes. BaseDataModel.schema.CATEGORY2 = "category2" -- This section is deprecated. local INDEX_RECIPES = "recipes" local INDEX_RECIPE_GRADE = "grade" local INDEX_RECIPE_GRADE_ALT = "gradeId" -- alt for service buildings 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_STORAGE_TANK = "baseTankCapacity" --endregion --region Private constants ---Enum converts grade strings (keys) to numbers (values). ---@type table local LOOKUP_CONVERT_GRADE_TO_NUMBER = { ["Grade0"] = 0, ["Grade1"] = 1, ["Grade2"] = 2, ["Grade3"] = 3, } --endregion --region Localization string constants 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 --defined as @fields above in class definition --endregion --region Private methods ---Loads the JSON data into the two structured member variables for data access. --- ---(Benchmarking 2024-11-04: ~0.018 seconds for biggest data file) --- ---@param instance 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 id = record[instance.schema.ID] local name = record[instance.schema.NAME] instance.dataTable[id] = record instance.mapNamesToIDs[name] = id -- If derived classes have anything to do per-record, let them here. instance:initializeRecord(id) end --return instance.dataTable, instance.mapNamesToIDs --uncomment when debugging end ---Gets the record with that ID from the instance's data table. --- ---(Benchmarking 2024-11-04: ~0 seconds for biggest data file) --- ---@param instance BaseDataModel ---@param id buildingIDString ---@return buildingRecordTable 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] or nil end ---Gets the record with that display name from the instance's data table. If the data model has multiple buildings with the same name, this will return only one of them. --- ---(Benchmarking 2024-11-04: ~0.0000 seconds for biggest data file) --- ---@param instance BaseDataModel ---@param name string the display name ---@return buildingRecordTable 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] or nil end ---Deeply copies the provided table and returns the copy. Does not modify the original. --- ---@param original table to copy ---@return table the copy local function deepCopy(original) local copy if type(original) == "table" then copy = {} for key, value in pairs(original) do copy[key] = deepCopy(value) end else copy = original end return copy end --endregion --region Public constructor ---@public ---@constructor ---Instantiates a new BaseDataModel by loading the specified data file into the instance. --- ---(Benchmarking: ~0.0170 seconds with largest data file.) --- ---@param dataFile string filename of Json data to load into this instance ---@return BaseDataModel a new instance storing the specified data function BaseDataModel.new(dataFile) local instance = {} setmetatable(instance, { __index = BaseDataModel }) instance.INDEX = deepCopy(BaseDataModel.schema) load(instance, dataFile) return instance end --endregion --region Public building interface ---@protected ---@function ---Initializes the specified record. No-op for BaseDataModel. --- ---**Likely overridden in derived classes.** --- ---@param id buildingIDString function BaseDataModel:initializeRecord(id) -- No-op. Override in derived classes if needed. id = id end ---@public ---@function ---Gets the ID associated with the specified name in this instance's data. --- ---@param name string the display name ---@return buildingIDString or nil if not found function BaseDataModel:getID(name) local building = findName(self, name) if building then return building[self.schema.ID] else return nil end end ---@public ---@function ---Gets the category for the specified ID from this instance's data. --- ---@param id buildingIDString ---@return string category, or nil if not found function BaseDataModel:getCategory(id) local building = findID(self, id) if building then return building[self.schema.CATEGORY1] else return nil end end ---@public ---@function ---Gets the secondary category for the specified ID from this instance's data. --- ---@param id buildingIDString ---@return string secondary category, or nil if not found function BaseDataModel:getSecondCategory(id) local building = findID(self, id) if building then return building[self.schema.CATEGORY2] else return nil end end ---@public ---@function ---Gets the city score for the specified ID from this instance's data. --- ---@param id buildingIDString ---@return number city score, or nil if not found function BaseDataModel:getCityScore(id) local building = findID(self, id) if building then return building[self.schema.CITY_SCORE] else return nil end end ---@public ---@function ---Gets the construction goods for the specified ID from this instance's data. ---Returns the array of construction goods, straight from the table, in subtables of IDs and amounts. --- ---See the data schema for details. --- ---@param id buildingIDString ---@return constructionGoodsTable an array of construction goods' IDs and amounts, or nil if not found function BaseDataModel:getConstructionCosts(id) local building = findID(self, id) if building then return building[self.schema.CONSTRUCTION] else return nil end end ---@public ---@function ---Gets the time required for construction for the specified ID from this instance's data. --- ---@param id buildingIDString ---@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[self.schema.CONSTRUCTION_TIME] else return nil end end ---@public ---@function ---Gets the description for the specified ID from this instance's data. --- ---@param id buildingIDString ---@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[self.schema.DESCRIPTION] else return nil end end ---@public ---@function ---Gets the icon filename for the specified ID from this instance's data. --- ---**Likely overridden by derived classes.** --- ---@param id buildingIDString ---@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 ---@public ---@function ---Checks whether the specified ID is movable from this instance's data. --- ---@param id buildingIDString ---@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[self.schema.IS_MOVABLE] else return nil end end ---@public ---@function ---Gets the name for the specified ID from this instance's data. --- ---@param id buildingIDString ---@return string the name, or nil if not found function BaseDataModel:getName(id) local building = findID(self, id) if building then return building[self.schema.NAME] else return nil end end ---@public ---@function ---Gets the number of workplaces for the specified ID from this instance's data. --- ---@param id buildingIDString ---@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[self.schema.WORKPLACES] else return nil end end ---@public ---@function ---Gets the size for ths specified ID from this instance's data, expressed as an X-by-Y string. --- ---@param id buildingIDString ---@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[self.schema.SIZE_X] .. " × " .. building[self.schema.SIZE_Y] else return nil end end ---@public ---@function ---Gets the storage capacity for the specified ID from this instance's data. --- ---**Likely overridden by derived classes.** --- ---@param id buildingIDString ---@return number the storage capacity, or nil if not found function BaseDataModel:getStorage(id) local building = findID(self, id) if building then if building[self.schema.STORAGE_CAP] then return building[self.schema.STORAGE_CAP] elseif building[INDEX_STORAGE_TANK] then return building[INDEX_STORAGE_TANK] end else return nil end end ---@public ---@function ---Checks whether the specified ID is a service-type building from this instance's data. --- ---**Likely overridden by derived classes.** --- ---@param id buildingIDString ---@return boolean _true_ if a service building, _false_ if not, or nil if not found function BaseDataModel:isServiceBuilding(id) -- The retrieval isn't used, but it performs necessary error checking. local building = findID(self, id) if building then return false 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 buildingIDString ---@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 ---getIDsAndRecipesWhereProductIDAndIngredientID ---@param productID string the product ID ---@param ingredientID string the ingredient ID ---@return table array of pairs of buildingIDs and recipes, or {} if none found 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 with largest data file) --- ---@param productID string the product ---@param buildingID buildingIDString ---@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 with largest data file) --- ---@param ingredientID string ---@param buildingID buildingIDString ---@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 LOOKUP_CONVERT_GRADE_TO_NUMBER[recipeData.recipe[INDEX_RECIPE_GRADE]] elseif recipeData.recipe[INDEX_RECIPE_GRADE_ALT] then return LOOKUP_CONVERT_GRADE_TO_NUMBER[recipeData.recipe[INDEX_RECIPE_GRADE_ALT]] else -- Default of zero. return 0 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 return 0 end --endregion return BaseDataModel