Module:BaseDataModel: Difference between revisions

From Against the Storm Official Wiki
(Creating to help implement next version of data models)
 
m (changing constructor metatable usage)
Line 479: Line 479:


local instance = {}
local instance = {}
setmetatable(instance, BaseDataModel)
setmetatable(instance, { __index = BaseDataModel })
load(instance, dataFile)
load(instance, dataFile)



Revision as of 21:23, 24 October 2024

Documentation for this module may be created at Module:BaseDataModel/doc

--- @module BaseDataModel
local BaseDataModel = {}



--region Dependencies

local JsonUtils = require("Module:JsonUtils")

--endregion



--region Private constants

local INDEX_CATEGORY = "category"
local INDEX_CITY_SCORE = "cityScore"
local INDEX_CONSTRUCTION_TIME = "constructionTime"
local INDEX_DESCRIPTION = "description"
local INDEX_NAME = "displayName"
local INDEX_ID = "id"
local INDEX_IS_ESSENTIAL = "initiallyEssential"
local INDEX_IS_MOVABLE = "movable"
local INDEX_RECIPES = "recipes"
local INDEX_RECIPE_GRADE = "grade"
local INDEX_RECIPE_INGREDIENTS = "ingredients"
local INDEX_RECIPE_INGREDIENT_OPTION_AMOUNT = "amount"
local INDEX_RECIPE_INGREDIENT_OPTION_ID = "name"
local INDEX_RECIPE_PRODUCT = "product"
local INDEX_RECIPE_PRODUCT_AMOUNT = "amount"
local INDEX_RECIPE_PRODUCT_ID = "name"
local INDEX_RECIPE_TIME = "productionTime"
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_Y = "sizeY"
local INDEX_STORAGE_CAP = "storage"
local INDEX_WORKPLACES = "workplaces"

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

--- Main data array, indexed by ID.
local dataTable

--- Lookup table to make searching by names instant.
local mapNamesToIDs

--endregion



--region Private methods

---load
---Loads the JSON data into the two structured member variables for data access.
---
---Benchmarking: ~0.015 seconds
---
---@param instance table an instance of 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

---findID
---called with self and an ID, gets the record with that ID from that instance
---@param instance table an instance of BaseDataModel
---@param id string the ID
---@return table the record with that ID from that instance, 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]
end

---findName
---called with self and a name gets the record with that name from that instance
---@param instance table an instance of BaseDataModel
---@param name string the display name
---@return table the record with that name, 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]
end

--endregion



--region Public building interface

-- All building data models implement this interface
-- getID(displayName)
-- getCategory(id)
-- 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
---@return string the ID, 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

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

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

---getConstructionCosts from the instance's data using an ID in the form of an array of short tables describing goods required for construction.
---	example = {
---		[1] = {
---			["amount"] = 5,
---			["name"] = "[Mat Processed] Planks", -- note that this is NOT a name, but an ID
---		},
---		[2] = {
---			["amount"] = 2,
---			["name"] = "[Mat Processed] Bricks", -- note that this is NOT a name, but an ID
---		},
---	}
---@param id string the ID
---@return table an array of tables with amount and ID
function BaseDataModel:getConstructionCosts(id)
	local building = findID(self, id)
	if building then
		return building[INDEX_CONSTRUCTION_GOODS]
	else
		return nil
	end
end

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

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

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

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

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

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

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

---getStorage from the instance's data using an ID
---@param id string the ID
---@return number the storage capacity, or nil if not found
function BaseDataModel:getStorage(id)
	local building = findID(self, id)
	if building then
		return building[INDEX_STORAGE_CAP]
	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].id = 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.
---
---@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 recipe[INDEX_RECIPE_PRODUCT][INDEX_RECIPE_PRODUCT_ID] == productID then
				table.insert(ret, {
					["id"] = id,
					["recipe"] = recipe,
				})
			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.
---
---@param buildingID string the building
---@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, {
			["id"] = 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.
---
---@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
			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, {
							["id"] = id,
							["recipe"] = recipe,
						})
						break
					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.
---
---@param productID string the product
---@param buildingID string the building
---@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 recipe[INDEX_RECIPE_PRODUCT][INDEX_RECIPE_PRODUCT_ID] == productID then
			table.insert(ret, {
				["id"] = buildingID,
				["recipe"] = recipe,
			})
			return ret
		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.
---
---@param ingredientID string
---@param buildingID string
---@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
		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, {
						["id"] = buildingID,
						["recipe"] = recipe,
					})
					break
				end
			end
		end
	end
	return ret
end

--endregion



--region Public recipe interface

-- Methods in this interface take a recipe taken from this data model and return specific information within. This module owns the access to the inner data.

--endregion



--region Public constructor

---new
---Constructs a new BaseDataModel by loading the specified data file into the instance.
---
---Benchmarking: ~0.0151 seconds
---
---@param dataFile string filename of Json data to load into this instance
function BaseDataModel.new(dataFile)

	local instance = {}
	setmetatable(instance, { __index = BaseDataModel })
	load(instance, dataFile)

	return instance
end

--endregion

return BaseDataModel