Module:BaseDataModel: Difference between revisions
From Against the Storm Official Wiki
(Refactoring and significantly improving luadoc; no functional changes.) |
(Prepping for better module inheritance and robustness. no functional changes yet) |
||
Line 26: | Line 26: | ||
---@field dataTable table Primary data table of _buildingRecordTables_, keyed by _buildingIDString_ | ---@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 mapNamesToIDs table Lookup table, returns a _buildingIDString_ from a building's display name | ||
---@field | ---@field schema table data schema, with field names and subtables of field names | ||
local BaseDataModel = {} | local BaseDataModel = {} | ||
Line 41: | Line 41: | ||
---@protected | ---@protected | ||
---Protected data schema defining Json field names for accessing data. | ---Protected data schema defining Json field names for accessing data. | ||
---@field | ---@field schema table data schema, with field names and subtables of field names | ||
BaseDataModel. | BaseDataModel.schema = { | ||
ID = "id", | ID = "id", | ||
NAME = "displayName", | NAME = "displayName", | ||
CATEGORY1 = "category", | |||
CITY_SCORE = "cityScore", | CITY_SCORE = "cityScore", | ||
CONSTRUCTION_TIME = "constructionTime", | CONSTRUCTION_TIME = "constructionTime", | ||
Line 84: | Line 84: | ||
} | } | ||
---Secondary category, to be defined by derived classes. | |||
BaseDataModel.schema.CATEGORY2 = "category2" | |||
-- This section is deprecated. | |||
local INDEX_RECIPES = "recipes" | local INDEX_RECIPES = "recipes" | ||
local INDEX_RECIPE_GRADE = "grade" | local INDEX_RECIPE_GRADE = "grade" | ||
Line 105: | Line 104: | ||
local INDEX_RECIPE_HARVESTING_TIME = "harvestingTime" | local INDEX_RECIPE_HARVESTING_TIME = "harvestingTime" | ||
local INDEX_RECIPE_GATHERING_TIME = "gatheringTime" | local INDEX_RECIPE_GATHERING_TIME = "gatheringTime" | ||
local INDEX_STORAGE_TANK = "baseTankCapacity" | local INDEX_STORAGE_TANK = "baseTankCapacity" | ||
--endregion | --endregion | ||
Line 117: | Line 111: | ||
--region Private constants | --region Private constants | ||
---Enum converts grade strings (keys) to numbers (values). | ---Enum converts grade strings (keys) to numbers (values). | ||
Line 132: | Line 122: | ||
--endregion | --endregion | ||
--region Localization string constants | --region Localization string constants | ||
Line 164: | Line 156: | ||
instance.mapNamesToIDs = {} | instance.mapNamesToIDs = {} | ||
for _, record in ipairs(rawTable) do | for _, record in ipairs(rawTable) do | ||
local | local id = record[instance.schema.ID] | ||
local name = record[ | local name = record[instance.schema.NAME] | ||
instance.dataTable[ | instance.dataTable[id] = record | ||
instance.mapNamesToIDs[name] = | instance.mapNamesToIDs[name] = id | ||
-- If derived classes have anything to do per-record, let them here. | |||
instance:initializeRecord(id) | |||
end | end | ||
--return instance.dataTable, instance.mapNamesToIDs --uncomment when debugging | --return instance.dataTable, instance.mapNamesToIDs --uncomment when debugging | ||
Line 206: | Line 201: | ||
end | end | ||
return instance.dataTable[id] or nil | 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 | end | ||
Line 213: | Line 251: | ||
--region Public building interface | --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 | ---@public | ||
Line 224: | Line 274: | ||
local building = findName(self, name) | local building = findName(self, name) | ||
if building then | if building then | ||
return building[ | return building[self.schema.ID] | ||
else | else | ||
return nil | return nil | ||
Line 239: | Line 289: | ||
local building = findID(self, id) | local building = findID(self, id) | ||
if building then | if building then | ||
return building[ | return building[self.schema.CATEGORY1] | ||
else | else | ||
return nil | return nil | ||
end | end | ||
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 | ---@public | ||
Line 254: | Line 321: | ||
local building = findID(self, id) | local building = findID(self, id) | ||
if building then | if building then | ||
return building[ | return building[self.schema.CITY_SCORE] | ||
else | else | ||
return nil | return nil | ||
Line 272: | Line 339: | ||
local building = findID(self, id) | local building = findID(self, id) | ||
if building then | if building then | ||
return building[ | return building[self.schema.CONSTRUCTION] | ||
else | else | ||
return nil | return nil | ||
Line 287: | Line 354: | ||
local building = findID(self, id) | local building = findID(self, id) | ||
if building then | if building then | ||
return building[ | return building[self.schema.CONSTRUCTION_TIME] | ||
else | else | ||
return nil | return nil | ||
Line 302: | Line 369: | ||
local building = findID(self, id) | local building = findID(self, id) | ||
if building then | if building then | ||
return building[ | return building[self.schema.DESCRIPTION] | ||
else | else | ||
return nil | return nil | ||
Line 334: | Line 401: | ||
local building = findID(self, id) | local building = findID(self, id) | ||
if building then | if building then | ||
return building[ | return building[self.schema.IS_MOVABLE] | ||
else | else | ||
return nil | return nil | ||
Line 349: | Line 416: | ||
local building = findID(self, id) | local building = findID(self, id) | ||
if building then | if building then | ||
return building[ | return building[self.schema.NAME] | ||
else | else | ||
return nil | return nil | ||
Line 364: | Line 431: | ||
local building = findID(self, id) | local building = findID(self, id) | ||
if building then | if building then | ||
return building[ | return building[self.schema.WORKPLACES] | ||
else | else | ||
return nil | return nil | ||
Line 379: | Line 446: | ||
local building = findID(self, id) | local building = findID(self, id) | ||
if building then | if building then | ||
return building[ | return building[self.schema.SIZE_X] .. " × " .. building[self.schema.SIZE_Y] | ||
else | else | ||
return nil | return nil | ||
Line 396: | Line 463: | ||
local building = findID(self, id) | local building = findID(self, id) | ||
if building then | if building then | ||
if building[ | if building[self.schema.STORAGE_CAP] then | ||
return building[ | return building[self.schema.STORAGE_CAP] | ||
elseif building[INDEX_STORAGE_TANK] then | elseif building[INDEX_STORAGE_TANK] then | ||
return building[INDEX_STORAGE_TANK] | return building[INDEX_STORAGE_TANK] | ||
end | 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 | else | ||
return nil | return nil | ||
Line 718: | Line 803: | ||
--region Public recipe data retrieval interface | --region Public recipe data retrieval interface | ||
---@protected | |||
---@function | |||
function BaseDataModel.getProductID(recipe) | |||
-- TODO next | |||
end | |||
-- 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 794: | Line 885: | ||
elseif recipeData.recipe[INDEX_RECIPE_GRADE_ALT] then | elseif recipeData.recipe[INDEX_RECIPE_GRADE_ALT] then | ||
return LOOKUP_CONVERT_GRADE_TO_NUMBER[recipeData.recipe[INDEX_RECIPE_GRADE_ALT]] | return LOOKUP_CONVERT_GRADE_TO_NUMBER[recipeData.recipe[INDEX_RECIPE_GRADE_ALT]] | ||
else | |||
-- Default of zero. | |||
return 0 | |||
end | end | ||
end | end | ||
Line 917: | Line 1,011: | ||
return BaseDataModel | return BaseDataModel |
Revision as of 02:44, 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", TANK = "baseTankCapacity", 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 ---@protected ---@function function BaseDataModel.getProductID(recipe) -- TODO next end -- 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