Module:GoodsData

From Against the Storm Official Wiki
Revision as of 00:17, 17 November 2023 by Aeredor (talk | contribs) (Now data table is indexed with keys instead of just integers, should be easier to use now. Refactored code; added getter methods; reordered methods, improved documentation and commenting)

Overview

Module for compiling goods (or resources as some call them) information from wiki data sources. Restructures the flat data tables that are produced from CsvUtils to make them more conducive to Lua methods that need to display the information.

Usage

The standard way of using this module is a call like the following:

iconFilename = GoodsData.getGoodIcon(goodName)

This will return a string corresponding to the filename of the good, including the .png extension. It is preferable to call the getter methods with the name of the good rather than retrieving the entire record for the good, so that your code stays protected from any variations in this module. The getter methods are all called with the plain-language name of the good, as spelled in the game.

longDescription = GoodsData.getGoodDescription(goodName)
inventoryCategory = GoodsData.getGoodCategory(goodName)
isEatable = GoodsData.isGoodEatable(goodName)
isBurnable = GoodsData.isGoodBurnable(goodName)
burnSeconds = GoodsData.getGoodBurnTime(goodName)
amberSellValue, amberBuyValue = GoodsData.getGoodValue(goodName)
iconFilename = GoodsData.getGoodIcon(goodName)

As a last resort, or if you need to transform the data structure, you can call the method getAllDataForGood(displayName). This returns a whole record from the data table corresponding to the requested display name.

The data table for goods has the following structure:

goodsTable = {
	["good1_ID"] = {
		["id"] = "good1_ID",
		["displayName"] = "Plain Language Name",
		["description"] = "A long string with some HTML entities too.",
		["category"] = "Inventory category",
		["eatable"] = true or false if food
		["canBeBurned"] = true or false if fuel and sacrifice
		["burningTime"] = 99, number of seconds
		["tradingSellValue"] = 99.99, in amber
		["tradingBuyValue"] = 99.99, in amber
		["iconName"] = "Icon_Resource_filename.png" including PNG extension
	},
	["good2_ID"] = {
		...
	},
	["good3_ID"] = {
		...
	},
	...
}

---
--- Module for compiling goods (or resources as some call them) information
--- from wiki data sources. Restructures the flat data tables that are produced
--- from CsvUtils to make them more conducive to Lua methods that need to
--- display the information.
---
--- The standard way of using this module is a call like the following:
---
--- iconFilename = GoodsData.getGoodIcon(goodName)
---
--- This will return a string corresponding to the filename of the good. It is
--- preferable to call the getter methods with the name of the good rather than
--- retrieving the entire record for the good, so that your code stays
--- protected from any variations in this module. The getter methods are all
--- called with the plain-language name of the good, as spelled in the game.
---
--- * longDescription = GoodsData.getGoodDescription(goodName)
--- * inventoryCategory = GoodsData.getGoodCategory(goodName)
--- * isEatable = GoodsData.isGoodEatable(goodName)
--- * isBurnable = GoodsData.isGoodBurnable(goodName)
--- * burnSeconds = GoodsData.getGoodBurnTime(goodName)
--- * amberSellValue, amberBuyValue = GoodsData.getGoodValue(goodName)
--- * iconFilename = GoodsData.getGoodIcon(goodName)
---
--- As a last resort, or if you need to transform the data structure, you can
--- call the method getAllDataForGood(displayName). This returns a whole record
--- from the data table corresponding to the requested display name.
---
--- The data table for goods has the following structure:
---
--- goodsTable = {
---		["good1_ID"] = {
--- 		["id"] = "good1_ID",
--- 		["displayName"] = "Plain Language Name",
--- 		["description"] = "A long string with some HTML entities too.",
--- 		["category"] = "Inventory category",
--- 		["eatable"] = true or false if food
---			["canBeBurned"] = true or false if fuel and sacrifice
---			["burningTime"] = 99, number of seconds
--- 		["tradingSellValue"] = 99.99, in amber
--- 		["tradingBuyValue"] = 99.99, in amber
--- 		["iconName"] = "Icon_Resource_filename.png" including PNG extension
---		},
--- 	["good2_ID"] = {
---			...
--- 	},
--- 	["good3_ID"] = {
---			...
--- 	},
--- 	...
--- }
---
--- @module GoodsData
local GoodsData = {}



local CsvUtils = require("Module:CsvUtils")



--region Private member variables

--- Main data table, like this: table[ID] = table containing data for that ID
local goodsTable

--- Lookup map. Built once and reused on all subsequent calls within this
--- session, like this: table[displayName] = goodID
local goodsNameToGoodID

--endregion



--region Private constants

local DATA_TEMPLATE_NAME = "Template:Goods_csv"

local HEADER_ROW = 1
local DATA_ROWS = 2

local ORIGINAL_INDEX_ID = 1

local INDEX_NAME = "displayName"
local INDEX_DESCRIPTION = "description"
local INDEX_CATEGORY = "category"
local INDEX_EATABLE = "eatable"
local INDEX_BURNABLE = "canBeBurned"
local INDEX_BURNTIME = "burningTime"
local INDEX_SELL_VALUE = "tradingSellValue"
local INDEX_BUY_VALUE = "tradingBuyValue"
local INDEX_ICON_FILENAME = "iconName"

--endregion



--region Private methods

---
--- Since the goods information is already flat and well structured, all this has
--- to do is swap out the integer keys for the good IDs and only return the data
--- rows, without the header row.
---
--- @param originalGoodsTable table of CSV-based data, with header row, data rows
--- @return table with new structure with IDs as keys
function restructureGoodsTable(originalGoodsTable)

	local newGoodsTable = {}
	for _, good in ipairs(originalGoodsTable[DATA_ROWS]) do

		-- Copy over the content, mapping unhelpful indexes into headers keys.
		local newGood = {}
		for index, key in pairs(originalGoodsTable[HEADER_ROW]) do

			newGood[key] = good[index]

			--Append file extension for this one field.
			if key == INDEX_ICON_FILENAME then
				newGood[key] = newGood[key] .. ".png"
			end
		end

		newGoodsTable[good[ORIGINAL_INDEX_ID]] = newGood
	end

	return newGoodsTable
end



---
--- Data loader function that uses the utility module and restructures the data
--- to be easier to access for invoking methods and later method calls. This
--- method is automatically called by all public member functions if the main
--- data table has not yet been populated in the current session.
function loadData()
	
	-- Utility module retrieves the data as basic, flat lua tables. We don't
	-- need to use the header lookup table that's returned with the data table.
	local originalGoodsTable, _ = CsvUtils.extractTables(DATA_TEMPLATE_NAME)
	
	-- Now restructure to be more conducive for data about goods.
	goodsTable = restructureGoodsTable(originalGoodsTable)
end



---
--- Uses the display name, which people are more familiar with, to find the
--- encoded ID of the good. Useful for retrieving good data that is indexed by
--- ID.
---
--- Returns nil if the good with the specified name is not found.
---
--- Builds a lookup table the first time it's called so it only has to iterate
--- over the whole data set once per session.
---
--- @param displayName string plain-language name of the good to find
--- @return string ID of the good found, or nil if not found
function findGoodIDByName(displayName)

	-- At runtime, this should never be nil or empty, so throw an error.
	if not displayName or displayName == "" then
		error("Parameter is nil or empty for the good's name: " .. displayName .. ".")
	end

	if not goodsTable then
		loadData()
	end

	local foundGoodID

	-- Decide whether we need to traverse the big table. If this isn't the first
	-- time this method is called, we can just look it up.
	if not goodsNameToGoodID then

		goodsNameToGoodID = {}
		for goodID, good in pairs(goodsTable) do
			-- Store it when we find it...
			if not foundGoodID and good[INDEX_NAME] == displayName then
				foundGoodID = goodID
			end
			--Then keep going to build the lookup table.
			goodsNameToGoodID[good[INDEX_NAME]] = goodID
		end
	else
		-- When we can, just get it from the lookup table.
		foundGoodID = goodsNameToGoodID[displayName]
	end

	return foundGoodID
end

--endregion



--region Public methods

---
--- Retrieve the whole table of data for the specified good. Instead of this,
--- you should probably be calling the individual getter methods.
---
--- Throws an error if called with nil or empty string. Returns nil if the
--- specified good cannot be found.
---
--- @param displayName string plain language name of the good
--- @return table containing the data for the specified good with key-value pairs, or nil if not found
function GoodsData.getAllDataForGood(displayName)

	-- At runtime, this should never be nil or empty.
	if not displayName or displayName == "" then
		error("Parameter is nil or empty for the good's name: " .. displayName .. ".")
	end

	if not goodsTable then
		loadData()
	end

	local goodID = findGoodIDByName(displayName)
	if not goodID then
		return nil
	end

	return goodsTable[goodID]
end



---
--- Retrieves the description for the good specified by its plain language
--- display name.
---
--- Returns nil if the good named was not found.
---
---@param displayName string with the plain language name of the good
---@return string the in-game description of the specified good
function GoodsData.getGoodDescription(displayName)

	local good = GoodsData.getAllDataForGood(displayName)

	if not good then
		return nil
	end

	return good[INDEX_DESCRIPTION]
end



---
--- Retrieves the inventory category for the good specified by its plain
--- language display name.
---
--- Returns nil if the good named was not found.
---
---@param displayName string with the plain language name of the good
---@return string the catgegory
function GoodsData.getGoodCategory(displayName)

	local good = GoodsData.getAllDataForGood(displayName)

	if not good then
		return nil
	end

	return good[INDEX_CATEGORY]
end



---
--- Retrieves whether the good is eatable, specified by its plain language
--- display name.
---
--- Returns nil if the good named was not found.
---
---@param displayName string with the plain language name of the good
---@return boolean true if the good counts as food
function GoodsData.isGoodEatable(displayName)

	local good = GoodsData.getAllDataForGood(displayName)

	if not good then
		return nil
	end

	return good[INDEX_EATABLE]
end



---
--- Retrieves whether the good is burnable, specified by its plain language
--- display name.
---
--- Returns nil if the good named was not found.
---
---@param displayName string with the plain language name of the good
---@return boolean true if the good counts as fuel
function GoodsData.isGoodBurnable(displayName)

	local good = GoodsData.getAllDataForGood(displayName)

	if not good then
		return nil
	end

	return good[INDEX_BURNABLE]
end



---
--- Retrieves the burn time for the good specified by its plain language
--- display name.
---
--- Returns nil if the good named was not found.
---
---@param displayName string with the plain language name of the good
---@return number of seconds the good burns as fuel
function GoodsData.getGoodBurnTime(displayName)

	local good = GoodsData.getAllDataForGood(displayName)

	if not good then
		return nil
	end

	return good[INDEX_BURNTIME]
end



---
--- Retrieves the trading value for the good specified by its plain language
--- display name.
---
--- Returns nil if the good named was not found.
---
---@param displayName string with the plain language name of the good
---@return number of amber the good sells for
---@return number of amber required to buy the good
function GoodsData.getGoodValue(displayName)

	local good = GoodsData.getAllDataForGood(displayName)

	if not good then
		return nil
	end

	return good[INDEX_SELL_VALUE], good[INDEX_BUY_VALUE]
end



---
--- Retrieves the icon filename for the good specified by its plain language
--- display name.
---
--- Returns nil if the good named was not found.
---
---@param displayName string with the plain language name of the good
---@return string the filename of the icon for the specified good
function GoodsData.getGoodIcon(displayName)

	local good = GoodsData.getAllDataForGood(displayName)

	if not good then
		return nil
	end

	return good[INDEX_ICON_FILENAME]
end

--endregion

return GoodsData