Module:BaseDataModel: Difference between revisions
From Against the Storm Official Wiki
(Adding method to get product + ingredient combos) |
(Refactoring and significantly improving luadoc; no functional changes.) |
||
Line 1: | Line 1: | ||
--- @ | ---@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 INDEX table data schema, with field names and subtables of field names | |||
local BaseDataModel = {} | local BaseDataModel = {} | ||
Line 5: | Line 32: | ||
--region Dependencies | --region Dependencies | ||
local JsonUtils = require("Module:JsonUtils") | |||
--endregion | |||
--region Protected data schema | |||
---@protected | |||
---Protected data schema defining Json field names for accessing data. | |||
---@field INDEX table data schema, with field names and subtables of field names | |||
BaseDataModel.INDEX = { | |||
ID = "id", | |||
NAME = "displayName", | |||
CATEGORY = "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", | |||
}, | |||
}, | |||
}, | |||
} | |||
local | local INDEX_NAME = "displayName" | ||
local INDEX_CONSTRUCTION_TIME = "constructionTime" | local INDEX_CONSTRUCTION_TIME = "constructionTime" | ||
local INDEX_DESCRIPTION = "description" | local INDEX_DESCRIPTION = "description" | ||
local INDEX_IS_MOVABLE = "movable" | local INDEX_IS_MOVABLE = "movable" | ||
local INDEX_RECIPES = "recipes" | local INDEX_RECIPES = "recipes" | ||
local INDEX_RECIPE_GRADE = "grade" | local INDEX_RECIPE_GRADE = "grade" | ||
local INDEX_RECIPE_GRADE_ALT = "gradeId" | local INDEX_RECIPE_GRADE_ALT = "gradeId" -- alt for service buildings | ||
local INDEX_RECIPE_INGREDIENTS = "ingredients" | local INDEX_RECIPE_INGREDIENTS = "ingredients" | ||
local INDEX_RECIPE_INGREDIENT_OPTION_AMOUNT = "amount" | local INDEX_RECIPE_INGREDIENT_OPTION_AMOUNT = "amount" | ||
Line 39: | Line 106: | ||
local INDEX_RECIPE_GATHERING_TIME = "gatheringTime" | local INDEX_RECIPE_GATHERING_TIME = "gatheringTime" | ||
local INDEX_CONSTRUCTION_GOODS = "requiredGoods" | local INDEX_CONSTRUCTION_GOODS = "requiredGoods" | ||
local INDEX_SIZE_X = "sizeX" | local INDEX_SIZE_X = "sizeX" | ||
local INDEX_SIZE_Y = "sizeY" | local INDEX_SIZE_Y = "sizeY" | ||
Line 47: | Line 112: | ||
local INDEX_WORKPLACES = "workplaces" | local INDEX_WORKPLACES = "workplaces" | ||
local | --endregion | ||
--region Private constants | |||
---Shortcut to BaseDataModel.INDEX | |||
local INDEX = BaseDataModel.INDEX | |||
---Enum converts grade strings (keys) to numbers (values). | |||
---@type table | |||
local LOOKUP_CONVERT_GRADE_TO_NUMBER = { | |||
["Grade0"] = 0, | ["Grade0"] = 0, | ||
["Grade1"] = 1, | ["Grade1"] = 1, | ||
Line 53: | Line 130: | ||
["Grade3"] = 3, | ["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" | 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" | ||
Line 61: | Line 142: | ||
--region Private member variables | --region Private member variables | ||
--defined as @fields above in class definition | |||
-- | |||
--endregion | --endregion | ||
Line 74: | Line 149: | ||
--region Private methods | --region Private methods | ||
---Loads the JSON data into the two structured member variables for data access. | ---Loads the JSON data into the two structured member variables for data access. | ||
--- | --- | ||
---Benchmarking: ~0. | ---(Benchmarking 2024-11-04: ~0.018 seconds for biggest data file) | ||
--- | --- | ||
---@param instance | ---@param instance BaseDataModel | ||
---@param dataFile string filename of Json data to load into this instance | ---@param dataFile string filename of Json data to load into this instance | ||
---@return table, table instance's data structures --when uncommented for debugging | ---@return table, table instance's data structures --when uncommented for debugging | ||
Line 90: | Line 164: | ||
instance.mapNamesToIDs = {} | instance.mapNamesToIDs = {} | ||
for _, record in ipairs(rawTable) do | for _, record in ipairs(rawTable) do | ||
local key = record[ | local key = record[INDEX.ID] | ||
local name = record[ | local name = record[INDEX.NAME] | ||
instance.dataTable[key] = record | instance.dataTable[key] = record | ||
instance.mapNamesToIDs[name] = key | instance.mapNamesToIDs[name] = key | ||
Line 98: | Line 172: | ||
end | end | ||
--- | ---Gets the record with that ID from the instance's data table. | ||
--- | |||
---@param instance | ---(Benchmarking 2024-11-04: ~0 seconds for biggest data file) | ||
---@param id | --- | ||
---@return | ---@param instance BaseDataModel | ||
---@param id buildingIDString | |||
---@return buildingRecordTable or nil if not found | |||
local function findID(instance, id) | local function findID(instance, id) | ||
Line 109: | Line 185: | ||
end | end | ||
return instance.dataTable[id] | return instance.dataTable[id] or nil | ||
end | 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. | ||
--- | |||
---@param instance | ---(Benchmarking 2024-11-04: ~0.0000 seconds for biggest data file) | ||
--- | |||
---@param instance BaseDataModel | |||
---@param name string the display name | ---@param name string the display name | ||
---@return | ---@return buildingRecordTable or nil if not found | ||
local function findName(instance, name) | local function findName(instance, name) | ||
Line 127: | Line 205: | ||
return nil | return nil | ||
end | end | ||
return instance.dataTable[id] | return instance.dataTable[id] or nil | ||
end | end | ||
Line 136: | Line 214: | ||
--region Public building interface | --region Public building interface | ||
-- | ---@public | ||
- | ---@function | ||
-- | ---Gets the ID associated with the specified name in this instance's data. | ||
- | --- | ||
-- | |||
---@param name string the display name | ---@param name string the display name | ||
---@return | ---@return buildingIDString or nil if not found | ||
function BaseDataModel:getID(name) | function BaseDataModel:getID(name) | ||
local building = findName(self, name) | local building = findName(self, name) | ||
if building then | if building then | ||
return building[ | return building[INDEX.ID] | ||
else | else | ||
return nil | return nil | ||
Line 162: | Line 230: | ||
end | end | ||
--- | ---@public | ||
---@param id | ---@function | ||
---@return string | ---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) | function BaseDataModel:getCategory(id) | ||
local building = findID(self, id) | local building = findID(self, id) | ||
if building then | if building then | ||
return building[ | return building[INDEX.CATEGORY] | ||
else | else | ||
return nil | return nil | ||
Line 174: | Line 245: | ||
end | end | ||
--- | ---@public | ||
---@param id | ---@function | ||
---@return number | ---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) | function BaseDataModel:getCityScore(id) | ||
local building = findID(self, id) | local building = findID(self, id) | ||
if building then | if building then | ||
return building[ | return building[INDEX.CITY_SCORE] | ||
else | else | ||
return nil | return nil | ||
Line 186: | Line 260: | ||
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 | ||
--- | |||
--- | |||
---@param id | |||
---@return | |||
function BaseDataModel:getConstructionCosts(id) | function BaseDataModel:getConstructionCosts(id) | ||
local building = findID(self, id) | local building = findID(self, id) | ||
if building then | if building then | ||
return building[ | return building[INDEX.CONSTRUCTION] | ||
else | else | ||
return nil | return nil | ||
Line 208: | Line 278: | ||
end | end | ||
--- | ---@public | ||
---@param id | ---@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 | ---@return number the number of seconds, or nil if not found | ||
function BaseDataModel:getConstructionTime(id) | function BaseDataModel:getConstructionTime(id) | ||
local building = findID(self, id) | local building = findID(self, id) | ||
if building then | if building then | ||
return building[ | return building[INDEX.CONSTRUCTION_TIME] | ||
else | else | ||
return nil | return nil | ||
Line 220: | Line 293: | ||
end | end | ||
--- | ---@public | ||
---@param id | ---@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 | ---@return string the description including sprite markup, or nil if not found | ||
function BaseDataModel:getDescription(id) | function BaseDataModel:getDescription(id) | ||
local building = findID(self, id) | local building = findID(self, id) | ||
if building then | if building then | ||
return building[ | return building[INDEX.DESCRIPTION] | ||
else | else | ||
return nil | return nil | ||
Line 232: | Line 308: | ||
end | end | ||
--- | ---@public | ||
---@param id | ---@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 | ---@return string the icon filename, or nil if not found | ||
function BaseDataModel:getIcon(id) | function BaseDataModel:getIcon(id) | ||
Line 244: | Line 325: | ||
end | end | ||
--- | ---@public | ||
---@param id | ---@function | ||
---@return boolean | ---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) | function BaseDataModel:isMovable(id) | ||
local building = findID(self, id) | local building = findID(self, id) | ||
if building then | if building then | ||
return building[ | return building[INDEX.IS_MOVABLE] | ||
else | else | ||
return nil | return nil | ||
Line 256: | Line 340: | ||
end | end | ||
--- | ---@public | ||
---@param id | ---@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 | ---@return string the name, or nil if not found | ||
function BaseDataModel:getName(id) | function BaseDataModel:getName(id) | ||
local building = findID(self, id) | local building = findID(self, id) | ||
if building then | if building then | ||
return building[ | return building[INDEX.NAME] | ||
else | else | ||
return nil | return nil | ||
Line 268: | Line 355: | ||
end | end | ||
--- | ---@public | ||
---@param id | ---@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 | ---@return number how many worker slots, or nil if not found | ||
function BaseDataModel:getNumberOfWorkplaces(id) | function BaseDataModel:getNumberOfWorkplaces(id) | ||
local building = findID(self, id) | local building = findID(self, id) | ||
if building then | if building then | ||
return building[ | return building[INDEX.WORKPLACES] | ||
else | else | ||
return nil | return nil | ||
Line 280: | Line 370: | ||
end | end | ||
--- | ---@public | ||
---@param id | ---@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 | ---@return string as x-by-y, or nil if not found | ||
function BaseDataModel:getSize(id) | function BaseDataModel:getSize(id) | ||
local building = findID(self, id) | local building = findID(self, id) | ||
if building then | if building then | ||
return building[ | return building[INDEX.SIZE_X] .. " × " .. building[INDEX.SIZE_Y] | ||
else | else | ||
return nil | return nil | ||
Line 292: | Line 385: | ||
end | end | ||
--- | ---@public | ||
---@param id | ---@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 | ---@return number the storage capacity, or nil if not found | ||
function BaseDataModel:getStorage(id) | function BaseDataModel:getStorage(id) | ||
local building = findID(self, id) | local building = findID(self, id) | ||
if building then | if building then | ||
if building[ | if building[INDEX.STORAGE_CAP] then | ||
return building[ | return building[INDEX.STORAGE_CAP] | ||
elseif building[INDEX_STORAGE_TANK] then | elseif building[INDEX_STORAGE_TANK] then | ||
return building[INDEX_STORAGE_TANK] | return building[INDEX_STORAGE_TANK] | ||
Line 398: | Line 496: | ||
---Benchmarking: ~0.0000 seconds | ---Benchmarking: ~0.0000 seconds | ||
--- | --- | ||
---@param buildingID | ---@param buildingID buildingIDString | ||
---@return table array of pairs of buildingIDs and recipes, or {} if none found | ---@return table array of pairs of buildingIDs and recipes, or {} if none found | ||
function BaseDataModel:getIDsAndRecipesWhereBuildingID(buildingID) | function BaseDataModel:getIDsAndRecipesWhereBuildingID(buildingID) | ||
Line 459: | Line 557: | ||
end | 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) | function BaseDataModel:getIDsAndRecipesWhereProductIDAndIngredientID(productID, ingredientID) | ||
Line 518: | Line 620: | ||
---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. | ---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 | ---(Benchmarking: ~0.0000 seconds with largest data file) | ||
--- | --- | ||
---@param productID string the product | ---@param productID string the product | ||
---@param buildingID | ---@param buildingID buildingIDString | ||
---@return table array of pairs of buildingIDs and recipes, or {} if none found | ---@return table array of pairs of buildingIDs and recipes, or {} if none found | ||
function BaseDataModel:getIDsAndRecipesWhereProductIDAndBuildingID(productID, buildingID) | function BaseDataModel:getIDsAndRecipesWhereProductIDAndBuildingID(productID, buildingID) | ||
Line 573: | Line 675: | ||
---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. | ---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 | ---(Benchmarking: ~0.0000 seconds with largest data file) | ||
--- | --- | ||
---@param ingredientID string | ---@param ingredientID string | ||
---@param buildingID | ---@param buildingID buildingIDString | ||
---@return table array of pairs of buildingIDs and recipes, or {} if none found | ---@return table array of pairs of buildingIDs and recipes, or {} if none found | ||
function BaseDataModel:getIDsAndRecipesWhereIngredientIDAndBuildingID(ingredientID, buildingID) | function BaseDataModel:getIDsAndRecipesWhereIngredientIDAndBuildingID(ingredientID, buildingID) | ||
Line 688: | Line 790: | ||
if recipeData.recipe[INDEX_RECIPE_GRADE] then | if recipeData.recipe[INDEX_RECIPE_GRADE] then | ||
return | return LOOKUP_CONVERT_GRADE_TO_NUMBER[recipeData.recipe[INDEX_RECIPE_GRADE]] | ||
elseif recipeData.recipe[INDEX_RECIPE_GRADE_ALT] then | elseif recipeData.recipe[INDEX_RECIPE_GRADE_ALT] then | ||
return | return LOOKUP_CONVERT_GRADE_TO_NUMBER[recipeData.recipe[INDEX_RECIPE_GRADE_ALT]] | ||
end | end | ||
end | end | ||
Line 808: | Line 910: | ||
return recipeData.recipe[INDEX_RECIPE_ING_SERVICE_GOODS][i][INDEX_RECIPE_INGREDIENT_OPTION_AMOUNT] | return recipeData.recipe[INDEX_RECIPE_ING_SERVICE_GOODS][i][INDEX_RECIPE_INGREDIENT_OPTION_AMOUNT] | ||
end | end | ||
return 0 | |||
end | end | ||
Line 816: | Line 920: | ||
--region Public constructor | --region Public constructor | ||
--- | ---@public | ||
--- | ---@constructor | ||
---Instantiates a new BaseDataModel by loading the specified data file into the instance. | |||
--- | --- | ||
---Benchmarking: ~0. | ---(Benchmarking: ~0.0170 seconds with largest data file.) | ||
--- | --- | ||
---@param dataFile string filename of Json data to load into this instance | ---@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) | function BaseDataModel.new(dataFile) | ||
Revision as of 21:57, 4 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 INDEX 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 INDEX table data schema, with field names and subtables of field names BaseDataModel.INDEX = { ID = "id", NAME = "displayName", CATEGORY = "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", }, }, }, } local INDEX_NAME = "displayName" local INDEX_CONSTRUCTION_TIME = "constructionTime" local INDEX_DESCRIPTION = "description" local INDEX_IS_MOVABLE = "movable" 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_CONSTRUCTION_GOODS = "requiredGoods" local INDEX_SIZE_X = "sizeX" local INDEX_SIZE_Y = "sizeY" local INDEX_STORAGE_CAP = "storage" local INDEX_STORAGE_TANK = "baseTankCapacity" local INDEX_WORKPLACES = "workplaces" --endregion --region Private constants ---Shortcut to BaseDataModel.INDEX local INDEX = BaseDataModel.INDEX ---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 key = record[INDEX.ID] local name = record[INDEX.NAME] instance.dataTable[key] = record instance.mapNamesToIDs[name] = key end --return instance.dataTable, instance.mapNamesToIDs --uncomment when debugging end ---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 --endregion --region Public building interface ---@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[INDEX.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[INDEX.CATEGORY] 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[INDEX.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[INDEX.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[INDEX.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[INDEX.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[INDEX.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[INDEX.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[INDEX.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[INDEX.SIZE_X] .. " × " .. building[INDEX.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[INDEX.STORAGE_CAP] then return building[INDEX.STORAGE_CAP] elseif building[INDEX_STORAGE_TANK] then return building[INDEX_STORAGE_TANK] end else return nil end end --endregion --region Public building recipe query interface -- All methods in the Building recipe query interface returns an array of pairs of building IDs and recipes, pulled straight from the JSON-based data table: -- [1] = { -- [1].buildingID = string --building ID -- [1].recipe = { -- ["product"] = { --product info -- ["name"] = string --product ID *NOT* name! -- ["amount"] = number -- } -- ["grade"] = string --like "Grade2" -- ["productionTime"] = number --in seconds -- ["ingredients"] = { --ingredient slots (between 0-3) -- [1] = { --option list (between 1-6) -- [1] = { --option info -- ["name"] = string --ingredient ID *NOT* name -- ["amount"] = number -- }, -- ... --next option -- }, -- ...--next ingredient slot -- } -- } -- } -- getIDsAndRecipesWhereProductID(productID) -- getIDsAndRecipesWhereBuildingID(buildingID) -- getIDsAndRecipesWhereIngredientID(ingredientID) -- getIDsAndRecipesWhereProductIDAndBuildingID(productID, buildingID) -- getIDsAndRecipesWhereIngredientIDAndBuildingID(ingredientID, buildingID) ---getIDsAndRecipesWhereProductID ---Looks through all the instance's buildings' recipes for any that have the specified product, and returns an array of pairs of building IDs and recipes. --- ---Handles when workshops and gathering huts name their fields differently. --- ---Benchmarking: ~0.0001 --- ---@param productID string the product ---@return table array of pairs of buildingIDs and recipes, or {} if none found function BaseDataModel:getIDsAndRecipesWhereProductID(productID) if not self.dataTable then error(ERROR_MESSAGE_INSTANCE_NOT_INITIALIZED) end local ret = {} for id, building in pairs(self.dataTable) do for _, recipe in ipairs(building[INDEX_RECIPES]) do -- If there's a product subtable with an amount. if recipe[INDEX_RECIPE_PRODUCT] then if recipe[INDEX_RECIPE_PRODUCT][INDEX_RECIPE_PRODUCT_ID] == productID then table.insert(ret, { ["buildingID"] = id, ["recipe"] = recipe, }) end -- There may also be seeked deposits. elseif recipe[INDEX_RECIPE_PRO_DEPOSITS] then if recipe[INDEX_RECIPE_PRO_DEPOSITS] == productID then table.insert(ret, { ["buildingID"] = id, ["recipe"] = recipe, }) end -- Or a service provided elseif recipe[INDEX_RECIPE_PRO_SERVICE] then if recipe[INDEX_RECIPE_PRO_SERVICE] == productID then table.insert(ret, { ["buildingID"] = id, ["recipe"] = recipe, }) end end end end return ret end ---getIDsAndRecipesWhereBuildingID ---Loads the instance's building's recipes into an array of pairs of the same building ID and those recipes. --- ---Benchmarking: ~0.0000 seconds --- ---@param buildingID 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]] 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 --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 }) load(instance, dataFile) return instance end --endregion return BaseDataModel