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


