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:
--- @module BaseDataModel
---@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


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",
},


--region Private constants
INGREDIENTS = "ingredients",
INGREDIENT = {
OPTION = {
ID = "name",
AMOUNT = "amount",
},
},
},
}


local INDEX_CATEGORY = "category"
local INDEX_NAME = "displayName"
local INDEX_CITY_SCORE = "cityScore"
local INDEX_CONSTRUCTION_TIME = "constructionTime"
local INDEX_CONSTRUCTION_TIME = "constructionTime"
local INDEX_DESCRIPTION = "description"
local INDEX_DESCRIPTION = "description"
local INDEX_NAME = "displayName"
 
local INDEX_ID = "id"
local INDEX_IS_ESSENTIAL = "initiallyEssential"
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_CONSTRUCTION_GOODS_AMOUNT = "amount"
local INDEX_CONSTRUCTION_GOODS_ID = "name"
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 CONVERT_GRADE_TO_NUMBER = {
--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
--- Main data array, indexed by ID.
local dataTable
 
--- Lookup table to make searching by names instant.
local mapNamesToIDs
 
--endregion
--endregion


Line 74: Line 149:
--region Private methods
--region Private methods


---load
---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.015 seconds
---(Benchmarking 2024-11-04: ~0.018 seconds for biggest data file)
---
---
---@param instance table an instance of BaseDataModel
---@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[INDEX_ID]
local key = record[INDEX.ID]
local name = record[INDEX_NAME]
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


---findID
---Gets the record with that ID from the instance's data table.
---called with self and an ID, gets the record with that ID from that instance
---
---@param instance table an instance of BaseDataModel
---(Benchmarking 2024-11-04:  ~0 seconds for biggest data file)
---@param id string the ID
---
---@return table the record with that ID from that instance, or nil if not found
---@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


---findName
---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.
---called with self and a name gets the record with that name from that instance
---
---@param instance table an instance of BaseDataModel
---(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 table the record with that name, or nil if not found
---@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


-- All building data models implement this interface
---@public
-- getID(displayName)
---@function
-- getCategory(id)
---Gets the ID associated with the specified name in this instance's data.
-- getCityScore(id)
---
-- getConstructionCosts(id) -- returns { [goodName] = stack size }
-- getConstructionTime(id)
-- getDescription(id)
-- getIcon(id)
-- isMovable(id)
-- getName(id)
-- getNumberOfWorkplaces(id)
-- getSize(id) -- returns string "X x Y"
-- getStorage(id)
 
---getID finds the specified name in the instance's data and returns the associated ID
---@param name string the display name
---@param name string the display name
---@return string the ID, or nil if not found
---@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[INDEX_ID]
return building[INDEX.ID]
else
else
return nil
return nil
Line 162: Line 230:
end
end


---getCategory from the instance's data using an ID
---@public
---@param id string the ID
---@function
---@return string the category, or nil if not found
---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[INDEX_CATEGORY]
return building[INDEX.CATEGORY]
else
else
return nil
return nil
Line 174: Line 245:
end
end


---getCityScore from the instance's data using an ID
---@public
---@param id string the ID
---@function
---@return number the city score, or nil if not found
---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[INDEX_CITY_SCORE]
return building[INDEX.CITY_SCORE]
else
else
return nil
return nil
Line 186: Line 260:
end
end


---getConstructionCosts from the instance's data using an ID in the form of an array of short tables describing goods required for construction.
---@public
--- example = {
---@function
--- [1] = {
---Gets the construction goods for the specified ID from this instance's data.
--- ["amount"] = 5,
---Returns the array of construction goods, straight from the table, in subtables of IDs and amounts.
--- ["name"] = "[Mat Processed] Planks", -- note that this is NOT a name, but an ID
---
--- },
---See the data schema for details.
--- [2] = {
---
--- ["amount"] = 2,
---@param id buildingIDString
--- ["name"] = "[Mat Processed] Bricks", -- note that this is NOT a name, but an ID
---@return constructionGoodsTable an array of construction goods' IDs and amounts, or nil if not found
--- },
--- }
---@param id string the ID
---@return table an array of tables with amount and ID
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[INDEX_CONSTRUCTION_GOODS]
return building[INDEX.CONSTRUCTION]
else
else
return nil
return nil
Line 208: Line 278:
end
end


---getConstructionTime from the instance's data using an ID
---@public
---@param id string the 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[INDEX_CONSTRUCTION_TIME]
return building[INDEX.CONSTRUCTION_TIME]
else
else
return nil
return nil
Line 220: Line 293:
end
end


---getDescription from the instance's data using an ID
---@public
---@param id string the 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[INDEX_DESCRIPTION]
return building[INDEX.DESCRIPTION]
else
else
return nil
return nil
Line 232: Line 308:
end
end


---getIcon from the instance's data using an ID
---@public
---@param id string the 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


---isMovable from the instance's data using an ID
---@public
---@param id string the ID
---@function
---@return boolean true if the building can be moved, false if not (or nil if not found)
---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[INDEX_IS_MOVABLE]
return building[INDEX.IS_MOVABLE]
else
else
return nil
return nil
Line 256: Line 340:
end
end


---getName from the instance's data using an ID
---@public
---@param id string the 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[INDEX_NAME]
return building[INDEX.NAME]
else
else
return nil
return nil
Line 268: Line 355:
end
end


---getNumberOfWorkplaces from the instance's data using an ID
---@public
---@param id string the 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[INDEX_WORKPLACES]
return building[INDEX.WORKPLACES]
else
else
return nil
return nil
Line 280: Line 370:
end
end


---getSize from the instance's data using an ID
---@public
---@param id string the 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[INDEX_SIZE_X] .. " × " .. building[INDEX_SIZE_Y]
return building[INDEX.SIZE_X] .. " × " .. building[INDEX.SIZE_Y]
else
else
return nil
return nil
Line 292: Line 385:
end
end


---getStorage from the instance's data using an ID
---@public
---@param id string the 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[INDEX_STORAGE_CAP] then
if building[INDEX.STORAGE_CAP] then
return building[INDEX_STORAGE_CAP]
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 string the building
---@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 string the building
---@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 string
---@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 CONVERT_GRADE_TO_NUMBER[recipeData.recipe[INDEX_RECIPE_GRADE]]
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 CONVERT_GRADE_TO_NUMBER[recipeData.recipe[INDEX_RECIPE_GRADE_ALT]]
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


---new
---@public
---Constructs a new BaseDataModel by loading the specified data file into the instance.
---@constructor
---Instantiates a new BaseDataModel by loading the specified data file into the instance.
---
---
---Benchmarking: ~0.0151 seconds
---(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