Pillars of Eternity Wiki
(Query and formatting to find containers with a specific item (with the calling template Template:Cargo container item))
(Corrected the way some probability figures were presented)
Line 176: Line 176:
 
local wrapper = mw.html.create("div")
 
local wrapper = mw.html.create("div")
 
:addClass("mw-collapsible mw-collapsed wikitable")
 
:addClass("mw-collapsible mw-collapsed wikitable")
:cssText("padding:10px; border-width: 1px; border-style: solid")
+
:cssText("padding:10px; border-width: 1px; border-style: solid; display: flow-root")
 
:wikitext("Containers with loot containing <b>[["..(results.item.page or results.item.name).."|"..results.item.name.."]]</b>")
 
:wikitext("Containers with loot containing <b>[["..(results.item.page or results.item.name).."|"..results.item.name.."]]</b>")
 
:tag("div")
 
:tag("div")
Line 397: Line 397:
 
-- Same occurance count, create array for results
 
-- Same occurance count, create array for results
 
if (occuranceCount == stats.bestContainersByProbabilityOccurances) then
 
if (occuranceCount == stats.bestContainersByProbabilityOccurances) then
table.insert(stats.bestContainersByProbability, i)
+
table.insert(stats.bestContainersByProbability, containers[i].guid)
 
else
 
else
stats.bestContainersByProbability = { i }
+
stats.bestContainersByProbability = { containers[i].guid }
 
end
 
end
 
 
Line 410: Line 410:
 
-- Same quantity, create array for results
 
-- Same quantity, create array for results
 
if (bestQty == stats.bestContainersByQuantityQty) then
 
if (bestQty == stats.bestContainersByQuantityQty) then
table.insert(stats.bestContainersByQuantity, i)
+
table.insert(stats.bestContainersByQuantity, containers[i].guid)
 
else
 
else
stats.bestContainersByQuantity = { i }
+
stats.bestContainersByQuantity = { containers[i].guid }
 
end
 
end
 
 
Line 421: Line 421:
 
end
 
end
 
 
  +
if (#containers > 0) then
for day = 1, 20 do
+
for day = 1, 20 do
if (stats.dayTotals[day] >= stats.bestDayQty) then
+
if ((stats.dayTotals[day] or 0) >= stats.bestDayQty) then
+
-- Same quantity, add to array for results
+
-- Same quantity, add to array for results
if (stats.dayTotals[day] == stats.bestDayQty) then
 
table.insert(stats.bestDays, day)
+
if (stats.dayTotals[day] == stats.bestDayQty) then
 
table.insert(stats.bestDays, day)
else
+
else
stats.bestDays = { day }
+
stats.bestDays = { day }
 
end
 
 
stats.bestDayQty = stats.dayTotals[day]
 
 
end
 
end
 
 
stats.bestDayQty = stats.dayTotals[day]
+
if ((stats.dayTotals[day] or 0) <= stats.worstDayQty) then
+
 
-- Same quantity, add to array for results
end
 
 
if (stats.dayTotals[day] == stats.worstDayQty) then
 
 
table.insert(stats.worstDays, day)
if (stats.dayTotals[day] <= stats.worstDayQty) then
 
+
else
  +
stats.worstDays = { day }
-- Same quantity, add to array for results
 
 
end
if (stats.dayTotals[day] == stats.worstDayQty) then
 
  +
table.insert(stats.worstDays, day)
 
  +
stats.worstDayQty = stats.dayTotals[day]
else
 
stats.worstDays = { day }
 
 
end
 
end
 
stats.worstDayQty = stats.dayTotals[day]
 
 
end
 
end
 
end
 
end
Line 473: Line 475:
 
end
 
end
 
 
function convertIndexesToLinks(indexes)
+
function findContainerByGuid(guid)
 
for i = 1, #results.containers do
  +
if (results.containers[i].guid == guid) then
 
return results.containers[i]
  +
end
  +
end
  +
  +
return nil
  +
end
  +
  +
function convertGuidsToLinks(guids)
 
local tbl = {}
 
local tbl = {}
  +
if (guids == nil or #guids == 0) then return nil end
for i = 1, #indexes do
 
  +
for i = 1, #guids do
local container = results.containers[indexes[i]]
 
  +
local container = findContainerByGuid(guids[i])
 
tbl[i] = "[["..container.page.."#"..container.guid.."|"..container.name.."]]"
 
tbl[i] = "[["..container.page.."#"..container.guid.."|"..container.name.."]]"
 
end
 
end
Line 483: Line 496:
 
 
 
local tbl = mw.html.create("table")
 
local tbl = mw.html.create("table")
:addClass("wikitable")
+
:addClass("wikitable mw-collapsible mw-collapsed")
 
:tag("tr")
 
:tag("tr")
 
:tag("th")
 
:tag("th")
Line 566: Line 579:
 
:done()
 
:done()
 
:tag("td")
 
:tag("td")
:node((#stats.bestContainersByProbability > 5) and "-" or table.concat(convertIndexesToLinks(stats.bestContainersByProbability), ", ").." ("..stats.bestContainersByProbabilityOccurances.." occurances)")
+
:node((#stats.bestContainersByProbability > 5) and "-" or table.concat(convertGuidsToLinks(stats.bestContainersByProbability), ", ").." ("..stats.bestContainersByProbabilityOccurances.." occurances)")
 
:done()
 
:done()
 
:done()
 
:done()
Line 578: Line 591:
 
:done()
 
:done()
 
:tag("td")
 
:tag("td")
:node((#stats.bestContainersByQuantity > 5) and "-" or table.concat(convertIndexesToLinks(stats.bestContainersByQuantity), ", ").." ("..stats.bestContainersByQuantityQty..")")
+
:node((#stats.bestContainersByQuantity > 5) and "-" or table.concat(convertGuidsToLinks(stats.bestContainersByQuantity), ", ").." ("..stats.bestContainersByQuantityQty..")")
 
:done()
 
:done()
 
:done()
 
:done()

Revision as of 14:32, 12 October 2021

Template-info Module documentation
Lua-Logo
This module depends on: Module:Arguments,Module:Common and Module:Yesno, with data stored in Module:Lootlist/data and Module:Lootlist/statedata. These scripts are written in Lua.

A complex module used to support the multiple lootlist templates.

Uses the following data modules:

See also


-- Module:Lootlist
-- Author: Macklin
-- Description: Used to generate/evaluate random lootlists and provides formatting
-- support for Template:Container, Template:ContanerList, and Template:ContainerTable

local p = {}
local cargo = mw.ext.cargo
local bit32 = require("bit32")
local common = require("Module:Common")
local data = nil

-- Make some commonly-used functions local to this module
local getArgs = require("Module:Arguments").getArgs
local isNilOrEmpty = common.isNilOrEmpty
local stringEndsWith = common.endsWith

-- Container functions

-- Entry point from Template:Container
-- 1. Calculates loot for each of the days and constructs a cargo store with the
-- parameters passed to the original template. Although we could just store the
-- results directly on the page, this saves a lot of space in comparison
-- 2. Creates and outputs what is actually shown on the page

-- Optional parameters:
-- cargo_store - If "yes" is passed, this container will be stored in cargo
-- display - Should be "box" for a ContainerList, "row" for a  ContainerTable, otherwise defaults to "none"
function p.Container(frame)
	
	local args = getArgs(frame)
	
	-- Fetch the parameters that were originally passed to the template containing the #invoke to this function
	-- We can't modify the frame args since they're read only, but we can make a copy of it
	local container = { }
	for k, v in pairs(args) do
		container[k] = v
	end
	
	-- Do checks to see if some required parameters are missing
	if		common.tableValueIsNilOrEmpty(container, "name")		then return
	elseif	common.tableValueIsNilOrEmpty(container, "position_x")	then return
	elseif	common.tableValueIsNilOrEmpty(container, "position_z")	then return
	end
	
	-- Fetch the root lootlist with this name from cargo
	local lootlist = GetLootlist(container["lootlist"])
	
	-- Not every container uses a lootlist. If one isn't present (i.e. no items or nil table) then skip generating the loot
	if (lootlist ~= nil and lootlist.items ~= nil) then
		
		-- These are two of the three parameters needed for seed generation, the last being the day of the month
		-- A fourth parameter, the player GameObject name's hashcode, is the same regardless and is defined in the InitSeed function
		local posx = tonumber(container["position_x"])
		local posz = tonumber(container["position_z"])
	
		-- Add a day_x_loot and day_x_loot_qty field for each day
		for day = 1, 20 do
			
			-- Initialize the Xorshift state for this container on this day
			local seed = CalculateSeedForContainer(posx, posz, day)
			Xorshift128_InitSeed(seed)
			
			-- Evaluate the lootlist
			local loot = EvaluateLootList(lootlist)
			
			-- Stack the items
			loot = StackItems(loot)
			
			-- Sort the items
			SortItems(loot)
			
			-- Add name, page, filters
			AddItemData(loot)
			
			container["day_"..day.."_loot"] = loot;
		end
	end
	
	-- Process fixed loot
	if (not isNilOrEmpty(container["inventory"])) then
		
		-- Create item objects by splitting the inventory and inventory_qty fields
		local inventory = { }
		local fixedInventoryIds = mw.text.split(container["inventory"], ";", true)
		local fixedInventoryQty = mw.text.split(container["inventory_qty"], ";", true)
		
		for i = 1, #fixedInventoryIds do
			inventory[i] = { }
			inventory[i].id = fixedInventoryIds[i]
			inventory[i].count = fixedInventoryQty[i]
		end
		
		-- Sort the items
		SortItems(inventory)
		
		-- Add name, page, filters
		AddItemData(inventory)
		
		-- Store in container for use in other functions
		container["inventory_items"] = inventory;
	end
	
	if (args["cargo_store"] == "yes") then
		ContainerCargoStore(frame, container)
	end
	
	local display = args["display"] or "none"
	
	if (display == "box") then
		return BuildContainerListBox(frame, container)
	elseif (display == "row") then
		return BuildContainerTableRow(frame, container)
	else
		return
	end
end

-- Performs a #cargo_store to store the Container in the associated cargo table. Does not return a value
-- Args should be a table containing all the fields passed to the original Container template, plus the additional day_x_loot tables
function ContainerCargoStore(frame, container)

	-- Because callParserFunction can only take string arguments, we remove all non-string fields from the container
	-- The day_x_loot fields need to be converted from tables into two separate "List (;) of String"'s before actually doing the cargo_store
	local storeArgs = { }
	
	for k, v in pairs(container) do
		
		local day = string.match(k, "day_(%d+)_loot.*")
		
		-- For all day_x_loot tables
		if (day ~= nil and #v > 0) then
			
			local lootStr = ""
			local lootQtyStr = ""
			
			-- Build up the day_x_loot string for day x
			-- Doing string concatenation with so few strings is in this case faster than constructing a table and doing table.concat
			for i, item in ipairs(v) do
				lootStr = lootStr..item.id..";"
				lootQtyStr = lootQtyStr..item.count..";"
			end
			
			storeArgs["day_"..day.."_loot"] = lootStr:sub(1, -2)
			storeArgs["day_"..day.."_loot_qty"] = lootQtyStr:sub(1, -2)
			
		-- For all other values (strings only)
		elseif (type(v) == "string") then
			storeArgs[k] = v
		end
	end
	
	storeArgs["_table"] = "Container_poe1"

	-- Call cargo store parser function
	frame:callParserFunction("#cargo_store:", storeArgs)
end

-- Entry point from Cargo container item.
function p.CargoContainerItem(frame)
	
	local args = getArgs(frame)
	
	if (isNilOrEmpty(args["item"])) then
		error("Parameter \"item\" must be set")
		return
	end
	
	-- Perform query
	local results = CargoContainerItemQuery(args["item"])
	
	-- Build HTML tables
	local queryTable = BuildContainerItemQueryTable(results.containers)
	local statTable = BuildContainerItemQueryStatisticsTable(results)
	
	-- Wrap table in collapsible
	local wrapper = mw.html.create("div")
		:addClass("mw-collapsible mw-collapsed wikitable")
		:cssText("padding:10px; border-width: 1px; border-style: solid; display: flow-root")
		:wikitext("Containers with loot containing <b>[["..(results.item.page or results.item.name).."|"..results.item.name.."]]</b>")
			:tag("div")
			:addClass("mw-collapsible-content")
			:cssText("overflow:hidden")
			:node(queryTable..statTable)
			:done()
			
	return tostring(wrapper)
end

-- Finds every container whose random or fixed item set contains a specific item
-- Takes a single argument, "item", the ID of the item to return results on
-- Returns:
-- "item" - an object containing the itemData 
-- "containers" - an array with the fields name, page, location, guid, and quantity - an array where each element is the number of the queried item that appears in that container on that day. Element 0 in quantity will be fixed loot, while element 20 is the quantity for day 20
-- "stats" - a set of interesting statistics
function CargoContainerItemQuery(item)

	-- Force to lower for comparison
	item = string.lower(item)
	
	local itemData = GetItemData(item)
	
	-- First, sanity check that the item actually exists in the item data
	if (itemData == nil) then
		error("No item with the ID "..item.." exists in the data module")
		return
	end
	
	local containers = {}
	
	-- Perform a separate query for each day (and "inventory", fixed days). This is nessecary because using multiple HOLDS breaks when there are multiple results on the same page (returning only the last result on a page). Additionally, using 20 LEFT OUTER JOINS in a single query is extremely slow, and often times out
	-- If a day contains multiple entries of the same item, a separate result for each entry will be returned
	for day = 0, 20 do
		
		-- 0 index is inventory, 1+ are days
		local name = (day == 0) and "inventory" or "day_"..day.."_loot"
		
		-- Construct the query
		local tables = "Container_poe1, Container_poe1__"..name..", Container_poe1__"..name.."_qty"
		local fields = "Container_poe1._ID=id, Container_poe1.name=name, Container_poe1._pageName=page, Container_poe1.location=location, Container_poe1.guid=guid, Container_poe1__"..name.."._value="..name..", Container_poe1__"..name.."_qty._value="..name.."_qty"
		local args = {}
		args.join = "Container_poe1._ID = Container_poe1__"..name.."._rowID, Container_poe1._ID = Container_poe1__"..name.."_qty._rowID"
		args.where = "Container_poe1__"..name.."._value = \""..item.."\" AND Container_poe1__"..name.."._position = Container_poe1__"..name.."_qty._position"
		args.limit = 0
		
		-- Perform the query
		local results = cargo.query(tables, fields, args)
		
		-- Process the results
		for r = 1, #results do
			
			-- Get the quantity
			local qty = tonumber(results[r][name.."_qty"])
			
			-- Nil supporting data
			results[r][name] = nil
			results[r][name.."_qty"] = nil
			
			-- Get existing data in output based on GUID (hopefully no containers have the same GUID)
			local data = common.tableGetTableWithKeyOfValue(containers, "guid", results[r].guid, true)
			
			-- If there is no existing data, create it
			if (data == nil) then
				data = mw.clone(results[r])
				
				-- An index-keyed table where each index is a quantity of the item on this day
				data.quantity = {}
				
				table.insert(containers, data)
			end
			
			-- Increment instead of just set, because there may be containers where a day contains multiple entries for the same item (this is certainly the case for some inventories)
			data.quantity[day] = data.quantity[day] ~= nil and (data.quantity[day] + qty) or qty
		end
		
		--[[
		
		-- HOLDS version of the above query section, is a little bit slower and doesn't select individual elements in the List (;) of String
		
		-- Construct the query
		local tables = "Container_poe1"
		local fields = "name, _pageName=page, location, guid, day_"..i.."_loot=day_"..i.."_loot, day_"..i.."_loot_qty=day_"..i.."_loot_qty"
		local args = {}
		args.where = "Container_poe1.day_"..i.."_loot HOLDS \""..item.."\""
		args.limit = 0
		
		-- Perform the query
		local results = cargo.query(tables, fields, args)
		
		--]]
		
	end
	
	-- Get some stats about the loot
	local stats = ContainerItemQueryStatistics(containers)
	
	return { item = itemData, containers = containers, stats = stats }
end


-- Returns a collection of interesting and/or useless statistics about the container query
function ContainerItemQueryStatistics(containers)
	
	local stats = {}
	
	stats.labels = {}
	
	-- The number of containers containing this item
	stats.numContainers = #containers
	stats.numContainersTotal = 0
	stats.numContainersPerc = 0
	
	-- The total amount of occurances of this item, an occurance being an instance on a day where this item will be found in a container. If a container has fixed loot containing this item, the container has 20 occurances
	stats.cumulativeOccurances  = 0
	
	-- The total amount of items that can be obtained for all days combined, including fixed loot (this is impossible though, since containers can't be looted more than once)
	stats.cumulativeQty = 0
	
	-- By selecting the best day for each container, this is the most amount of this item  that can obtained. This includes fixed loot
	stats.maximalQty = 0
	stats.maximalOccurances = 0
	
	-- By selecting the worst day for each container, this is the most amount of this item that can be obtained. This includes days with 0, and includes fixed loot
	-- This could also be seen as the guaranteed quantity.
	stats.minimalQty = 0
	stats.minimalOccurances = 0
	
	-- The best container for looting this item, this container has the most days where this item appears, fixed loot counts as one day
	stats.bestContainersByProbability = { }
	stats.bestContainersByProbabilityOccurances = 0
	
	-- The best container for looting this item, this container has the most amount of this item on a single day, including fixed loot
	stats.bestContainersByQuantity = { }
	stats.bestContainersByQuantityQty = 0
	
	-- An array containing the total amount of items looted from all containers on that day
	stats.dayTotals = {}
	
	-- The best days of the month for looting this item, the days with the highest total number of this item that will spawn
	stats.bestDays = { }
	
	-- Tracks the actual quantity of the above
	stats.bestDayQty = -math.huge
	
	-- The worst days of the month for looting this item, the days with the lowest total number of this item that will spawn
	stats.worstDays = { }
	
	-- Tracks the actual quantity of the above
	stats.worstDayQty = math.huge
	
	-- The percentage of loot outcomes that contain this item over all other outcomes. Every container has 20 outcomes representing each day
	stats.probability = 0
	
	for i = 1, #containers do
		
		local bestDay = 0
		local bestDayQty = 0 -- <- Not including fixed
		local worstDay = 0
		local worstDayQty = 0 -- <- Not including fixed
		
		local occuranceCount = 0
		local occuranceCountInclFixed = 0
		local occursAllDays = true -- <- Not including fixed
		
		local fixedLootQty = (containers[i].quantity[0] or 0)
		
		for day = 0, 20 do
			
			local qty = containers[i].quantity[day] or 0
			local qtyInclFixed = qty + fixedLootQty
			stats.cumulativeQty = stats.cumulativeQty + qtyInclFixed
			stats.dayTotals[day] = (stats.dayTotals[day] or 0) + qty
			
			if (day > 0) then
				
				-- Track day with the highest quantity
				if (qty > bestDayQty) then
					bestDay = day
					bestDayQty = qty
				end
				
				-- Track day with the lowest quantity
				if (qty < worstDayQty) then
					worstDay = day
					worstDayQty = qty
				end
				
				
				if (qty == 0) then
					-- Invalidate occursAllDays
					occursAllDays = false
				end
				
				-- Increment occurance count
				if (qty > 0) then
					occuranceCount = occuranceCount + 1
				end
				if (qty + fixedLootQty > 0) then
					occuranceCountInclFixed = occuranceCountInclFixed + 1
				end
				
			end
			
		end
		
		local bestQty = (bestDayQty + fixedLootQty)
		
		-- For this container, add the best day and fixed quantity
		stats.maximalQty = stats.maximalQty + bestDayQty + fixedLootQty
		stats.minimalQty = stats.minimalQty + (occursAllDays == true and worstDayQty or 0) + fixedLootQty
		
		stats.cumulativeOccurances = stats.cumulativeOccurances + occuranceCountInclFixed
		stats.maximalOccurances = stats.maximalOccurances + 1
		stats.minimalOccurances = stats.minimalOccurances + ((occursAllDays == true or fixedLootQty > 0) and 1 or 0)
		
		if (occuranceCount >= stats.bestContainersByProbabilityOccurances) then
			
			-- Same occurance count, create array for results
			if (occuranceCount == stats.bestContainersByProbabilityOccurances) then
				table.insert(stats.bestContainersByProbability, containers[i].guid)
			else
				stats.bestContainersByProbability = { containers[i].guid }
			end
			
			stats.bestContainersByProbabilityOccurances = occuranceCount
			
		end
		
		if (bestQty >= stats.bestContainersByQuantityQty) then
			
			-- Same quantity, create array for results
			if (bestQty == stats.bestContainersByQuantityQty) then
				table.insert(stats.bestContainersByQuantity, containers[i].guid)
			else
				stats.bestContainersByQuantity = { containers[i].guid }
			end
			
			stats.bestContainersByQuantityQty = bestQty
			
		end
		
	end
	
	if (#containers > 0) then
		for day = 1, 20 do
			if ((stats.dayTotals[day] or 0) >= stats.bestDayQty) then
				
				-- Same quantity, add to array for results
				if (stats.dayTotals[day] == stats.bestDayQty) then
					table.insert(stats.bestDays, day)
				else
					stats.bestDays = { day }
				end
				
				stats.bestDayQty = stats.dayTotals[day]
				
			end
			
			if ((stats.dayTotals[day] or 0) <= stats.worstDayQty) then
				
				-- Same quantity, add to array for results
				if (stats.dayTotals[day] == stats.worstDayQty) then
					table.insert(stats.worstDays, day)
				else
					stats.worstDays = { day }
				end
				
				stats.worstDayQty = stats.dayTotals[day]
			end
		end
	end
	
    local args = { limit = 0 }
    local results = cargo.query("Container_poe1", "COUNT(1)=count", args )
    stats.numContainersTotal = tonumber(results[1].count)
    stats.numContainersPerc = (stats.numContainers / stats.numContainersTotal) * 100
    stats.numContainersPerc = common.roundToDecimalPlaces(stats.numContainersPerc, 2)
    
	-- Determine the total number of outcomes by multiplying the number of containers by 20 (20 different outcomes for each day)
    stats.probability = (stats.cumulativeOccurances / (stats.numContainersTotal * 20)) * 100
    stats.probability = common.roundToDecimalPlaces(stats.probability, 2)
	
	return stats
end

function BuildContainerItemQueryStatisticsTable(results)
	
	local stats = results.stats
	
	function convertAllToOrdinal(days)
		local tbl = {}
		for i = 1, #days do
			tbl[i] = common.stringAddOrdinal(days[i])
		end
		return tbl
	end
	
	function findContainerByGuid(guid)
		for i = 1, #results.containers do
			if (results.containers[i].guid == guid) then
				return results.containers[i]
			end
		end
		
		return nil
	end
	
	function convertGuidsToLinks(guids)
		local tbl = {}
		if (guids == nil or #guids == 0) then return nil end
		for i = 1, #guids do
			local container = findContainerByGuid(guids[i])
			tbl[i] = "[["..container.page.."#"..container.guid.."|"..container.name.."]]"
		end
		return tbl
	end
	
	local tbl = mw.html.create("table")
	:addClass("wikitable mw-collapsible mw-collapsed")
	:tag("tr")
		:tag("th")
			:attr("colspan", "2")
			:css("text-align", "left")
			:node("Item statistics")
			:done()
		:done()
	:tag("tr")
		:tag("td")
			:node("Number of containers")
			:done()
		:tag("td"):node(stats.numContainers.." of "..stats.numContainersTotal.." ("..stats.numContainersPerc.."%)")
		:done()
	:tag("tr")
		:tag("td")
			:tag("span")
				:addClass("tooltip")
				:attr("title", "The percentage of loot list evaluations where this item occurs over all other outcomes across every container. Keep in mind that every container has exactly 20 outcomes representing each day. A container with fixed loot counts as 20 occurances of this item")
				:node("Item probability")
				:done()
			:done()
		:tag("td")
			:node(stats.probability.."% ("..stats.cumulativeOccurances.." of "..(stats.numContainersTotal * 20).." possible outcomes)")
			:done()
		:done()
	:tag("tr")
		:tag("td")
			:tag("span")
				:addClass("tooltip")
				:attr("title", "By selecting the best day for each container, this is the most amount of this item that can obtained after looting all containers. This includes fixed loot")
				:node("Maximum available items")
				:done()
			:done()
		:tag("td")
			:node(stats.maximalQty.." (in "..stats.maximalOccurances.." occurances)")
			:done()
		:done()
	:tag("tr")
		:tag("td")
			:tag("span")
				:addClass("tooltip")
				:attr("title", "By selecting the worst day for each container (including days with 0, always including fixed loot), this is the most amount of this item that can obtained after looting all containers. Also can be seen as the amount of guaranteed items.")
				:node("Minimum available items")
				:done()
			:done()
		:tag("td")
			:node(stats.minimalQty.." (in "..stats.minimalOccurances.." occurances)")
			:done()
		:done()
	:tag("tr")
		:tag("td")
			:tag("span")
				:addClass("tooltip")
				:attr("title", "The best days of the month for looting this item, i.e. the days with the highest number of this item that will spawn")
				:node("Best day(s)")
				:done()
			:done()
		:tag("td")
			:node((#stats.bestDays > 5) and "-" or table.concat(convertAllToOrdinal(stats.bestDays), ", ").." ("..stats.bestDayQty..")")
			:done()
		:done()
	:tag("tr")
		:tag("td")
			:tag("span")
				:addClass("tooltip")
				:attr("title", "The worst days of the month for looting this item, i.e. the days with the lowest number of this item that will spawn")
				:node("Worst day(s)")
				:done()
			:done()
		:tag("td")
			:node((#stats.worstDays > 5) and "-" or table.concat(convertAllToOrdinal(stats.worstDays), ", ").." ("..stats.worstDayQty..")")
			:done()
		:done()
	:tag("tr")
		:tag("td")
			:tag("span")
				:addClass("tooltip")
				:attr("title", "The containers with the highest probability of spawning the item. These containers have the most days where this item appears, ignoring fixed loot")
				:node("Best container(s) by probability")
				:done()
			:done()
		:tag("td")
			:node((#stats.bestContainersByProbability > 5) and "-" or table.concat(convertGuidsToLinks(stats.bestContainersByProbability), ", ").." ("..stats.bestContainersByProbabilityOccurances.." occurances)")
			:done()
		:done()
	:tag("tr")
		:tag("td")
			:tag("span")
				:addClass("tooltip")
				:attr("title", "The containers containing the most amount of this item on a single day, including fixed loot.")
				:node("Best container(s) by quantity")
				:done()
			:done()
		:tag("td")
			:node((#stats.bestContainersByQuantity > 5) and "-" or table.concat(convertGuidsToLinks(stats.bestContainersByQuantity), ", ").." ("..stats.bestContainersByQuantityQty..")")
			:done()
		:done()
	:done()
	
	return tostring(tbl)
end

	
-- Because cargo queries seem to be very expensive (in some cases adding tens of
-- seconds to the time it takes to evaluate loot lists for an area), cargo queries
-- have been replaced with simpler/faster lookups in an auxiliary data module which
-- gets fully loaded once for each page containing an #invoke to this module.

-- This replaces all queries to Lootlist_poe1, and queries to get filter strings
-- The only down-side is that the data module is static and will not update dynamically.
-- New items or changes to the associated fields will not be reflected in the table,
-- and will have to be manually regenerated by copying the output of this function
-- to Module:Lootlist/data

-- Depite performance being secondary to poe1 (since they screwed up (?) the RNG system
-- by making it depend on things that never change), it will be required for poe2
-- where the RNG depends on the player name - therefore we have to generate lists on
-- the fly via a JS invoke, and make the whole process take as little time as possible
function p.GenerateLootlistDataModule()
	
	-- First generate lootlist/lootitem data
	
	-- Get all lootitems. These will be sorted by lootlist then index/position
    local query = cargo.query("Lootitem_poe1", "lootlist, position, item,  is_lootlist, count, weight, always", { orderBy = "lootlist, position", limit = 10000 })
    
    local lootLists = {}
    
    -- Process the query items
	for _, item in ipairs(query) do
		
		local lootListStr = item["lootlist"];
		local lootList = common.tableGetTableWithKeyOfValue(lootLists, "id", lootListStr, false)
		
		-- Lootitems only have an internalname to identify them, since this is the only field we can rely on
		-- The page/item name is also in cargo, but we ignore this since they might not be accurate
		local newItem = { }
		newItem.id = common.ternary(common.isNilOrEmpty(item["item"]), nil, item["item"])
		newItem.isLootlist = common.ternary(item["is_lootlist"] ~= nil and item["is_lootlist"] == "1", true, false)
		newItem.count = item["count"]
		newItem.weight = item["weight"]
		newItem.always = common.ternary(item["always"] ~= nil and item["always"] == "1", true, false)
		
		-- Create new lootlist entry if one doesn't exist
		if (lootList == nil) then
		
			lootList =
			{
				id = lootListStr,
				items = { },
				totalWeight = 0,
			}
			
			table.insert(lootLists, lootList);
			
		end
		
		if (newItem.always == false) then
			lootList.totalWeight = lootList.totalWeight + newItem.weight
		end
		
		-- Add this item to the lootlist items (keeps in sequence)
		table.insert(lootList.items, tonumber(item["position"]) + 1, newItem)
	end
	
	-- Next generate item data, used to convert internalname to name, page
	-- also generate filter strings while we're at it
	
	local items = { }
	
	local args = { orderBy = "name", limit = 10000 }
	local queries = 
	{
		{ tbl = "Item_poe1", fields = "internalname, _pageName = page, name, LOWER(item_category) = category, LOWER(item_type) = type, is_unique" },
		{ tbl = "Shield_poe1", fields = "internalname, _pageName = page, name, CONCAT('shield') = category, LOWER(shield_type) = type, is_unique" },
		{ tbl = "Armor_poe1", fields = "internalname, _pageName = page, name, CONCAT('armor') = category, LOWER(armor_type) = type, is_unique" },
		{ tbl = "Weapon_poe1", fields = "internalname, _pageName = page, name, CONCAT('weapon') = category, LOWER(weapon_type) = type, is_unique" }
	}
	
	-- Perform a single query for each table retrieving all rows
	for q = 1, #queries do
	
		local results = cargo.query(queries[q].tbl, queries[q].fields, args)
		
		-- Add all items from the results
		for _, item in pairs(results) do
		
			if (item.internalname == nil) then
				error("No internalname found in "..queries[q].tbl.." for "..item.page.."!")
			end
			if (items[item.internalname] ~= nil) then
				error("Duplicate internalnames found in "..queries[q].tbl.." for "..item.internalname.." - "..items[item.internalname].page.." and "..item.page)
			end
			
			-- Names and pages are likely not escaped, so we have to do so manually
			item.name = string.format("%q", item.name)
			if (item.page ~= nil) then
				item.page = string.format("%q", item.page)
			end
			
			-- Create the filter string
			local filters = { item.category, item.type }
			if (item.is_unique == "yes") then table.insert(filters, "unique_item") end
			item.filters = table.concat(filters, ";")
			
			-- Instead of creating a new table for the item, we just modify the existing table in the query to suit
			-- nil'ing values that we no longer need
			item.category = nil
			item.type = nil
			item.is_unique = nil
			
			-- Only include the name, unless the page and name are different
			if (item.name == item.page) then
				item.page = nil
			end
			
			-- Force internalname lower
			local id = item.internalname:lower()
			item.internalname = nil
			
			-- Some items have multiple internalnames, which are typically separated by a <br/>
			-- For these, split into multiple separate items
			if (string.find(id, ".*<br/>.*") ~= nil) then
				local ids = mw.text.split(id, "<br/>")
				
				-- Add the item multiple times at each key
				for i = 1, #ids do
					ids[i] = mw.text.trim(ids[i]) -- Trim whitespace at ends of string
					items[ids[i]] = item
				end
				
			-- Only one internalname present
			else
				items[id] = item
			end
		end
		
	end
	
	-- Buffer output
	local buffer = { "local data =\n{\n" }
	
	-- Loot lists
	table.insert(buffer, "\tlootlists = \n\t{\n")
	
	for _, list in ipairs(lootLists) do
		
		table.insert(buffer, "\t\t[\""..list.id.."\"] =\n\t\t{\n")
		table.insert(buffer, "\t\t\ttotal_weight = "..list.totalWeight..",\n")
		table.insert(buffer, "\t\t\titems =\n\t\t\t{\n")
		
		for _, item in ipairs(list.items) do
			
			table.insert(buffer, "\t\t\t\t{ ")
			if (item.id == nil) then table.insert(buffer, "id = nil, ")
				                else table.insert(buffer, "id = \""..item.id.."\", ") end
			table.insert(buffer, "is_lootlist = "..tostring(item.isLootlist)..", ")
			table.insert(buffer, "weight = "..item.weight..", ")
			table.insert(buffer, "count = "..item.count..", ")
			table.insert(buffer, "always = "..tostring(item.always).." },\n")
		end
		
		table.insert(buffer, "\t\t\t}\n\t\t},\n")
		
	end
	
	table.insert(buffer, "\t},\n")
	
	-- Items
	table.insert(buffer, "\titems = \n\t{\n")
	
	for key, item in orderedPairs(items) do
		
		table.insert(buffer, "\t\t[\""..key.."\"] = { ")
		table.insert(buffer, "name = "..item.name.." , ")
		
		if (item.page ~= nil) then
			table.insert(buffer, "page = "..item.page..", ")
		end
		
		table.insert(buffer, "filters = \""..item.filters.."\" },\n")
	end
	
	table.insert(buffer, "\t},\n")
	table.insert(buffer, "}")
	
	return table.concat(buffer)
end

-- OUTPUT --
-- This section is for functions that output wikitext to be used on the page

-- Constructs the "row" wikitext used within the much more simple ContainerTable
function BuildContainerTableRow(frame, container)
	
	local iconGrid = BuildContainerIconGrid(container)
	local tooltip = BuildContainerTooltip(frame, container, iconGrid, "row")
					
	local filterStr = BuildContainerFilterString(container)
	
	local root = mw.html.create("tr")
	            :attr("id", container["guid"])
	            	-- Details cell (name + icon grid)
	        		:tag("td")
	        		:cssText("vertical-align:top")
						:tag("div"):cssText("display:flex; flex-direction:column; height:100%; min-width:165px")
    						:wikitext("<b>"..container["name"].."</b>"..tooltip)
        					:tag("div"):cssText("margin-top:auto; font-size:9px; color:grey; text-align:right;")
        						:wikitext(container["internalname"] or "")
        					:done()
    					:done()
					:done()
	        		-- Image cell
	        		:tag("td"):cssText("padding:0")
	        				  :wikitext(container["image"] and "[[File:"..container["image"].."|200px]]")
	        				  :done()
	        		-- Group + description cell
	        		:tag("td")
	        		:cssText("vertical-align:top")
	        				  :wikitext(container["description"] or "")
	        				  :done()
	        		
	-- Fixed items cell
	local fixedItems = root:tag("td"):cssText("vertical-align:top; font-size:smaller")
							   
	if (container["inventory_items"] ~= nil) then
		
		local fixedInventoryItems = container["inventory_items"]
		local fixedItemsList = fixedItems:tag("ul")
		
		-- Loop over items, adding a li element for each one
		for i = 1, #fixedInventoryItems do
			
			-- Create list item
			local li = mw.html.create("li")
				:addClass("loot-container-fixed-item")
				:attr("data-filters", fixedInventoryItems[i].filters)
			
			-- Only include quantity if it's > 1
			if (tonumber(fixedInventoryItems[i].count) > 1) then
				li:wikitext(fixedInventoryItems[i].count, "x ")
			end
			
			-- Add link 
			if (fixedInventoryItems[i].page ~= nil) then
				li:wikitext("[[", fixedInventoryItems[i].page, "|", fixedInventoryItems[i].name, "]]")
			else
				li:wikitext("[[", fixedInventoryItems[i].name, "]]")
			end
			
			-- Add list item to list
			fixedItemsList:node(li)
		end
	end
	
	-- Random items cells
	if (not isNilOrEmpty(container["lootlist"])) then
		
		-- Loop over all 20 day_x_loot tables
		for i = 1, 20 do
			
			local loot = container["day_"..i.."_loot"]
			
			-- Create a cell containing all the items on a specific day
			local dayLoot = root:tag("td")
				:addClass("lootlist-results-day")
				:cssText("vertical-align:top; font-size:smaller")
				:attr("data-day", tostring(i))
				:tag("ul")
			
			-- Create a list item for each loot item
			for _, item in ipairs(loot) do
				
				local li = mw.html.create("li"):addClass("lootlist-results-item")
				
				-- Add filter strings on an item level
				if (not isNilOrEmpty(item.filters)) then
					li:attr("data-filters", item.filters)
				end
				
				-- Quantity - Only include if > 1
				if (item.count > 1) then
					li:wikitext(item.count, "x ")
				end
				
				-- Link (add link label if page name exists)
				if (item.page ~= nil) then
					li:wikitext("[[", item.page, "|", item.name, "]]")
				else
					li:wikitext("[[", item.name, "]]")
				end
				
				-- Add list item to list
				dayLoot:node(li)
			end
		end
		
	-- If this container doesn't contain random loot, create a cell spanning 20 columns
	else
	
		root:tag("td"):attr("colspan", "20")
		
	end
	
	return tostring(root)
	
end

-- Construct the "box" wikitext to be used in Template:ContainerList
function BuildContainerListBox(frame, container)
	
	local iconRow = BuildContainerIconRow(container)
	local tooltip = BuildContainerTooltip(frame, container, iconRow, "box")
	local filterStr = BuildContainerFilterString(container)
	
	local header = "<b>"..container["name"].."</b>"
	--[[
	if (not isNilOrEmpty(container["description"])) then
		header = header.."<br/>"..container["description"]
	end
	]]--
	
	local mapNodeString = ""
	if (not isNilOrEmpty(container["mapnode"])) then
		mapNodeString = container["mapnode"].." ↲"
	end
		
	-- Begin creating loot container
	local tbl = mw.html.create("div")
		:addClass("loot-container")
		:attr("id", container["internalname"])
		:attr("data-name", container["name"])
		:attr("data-group", container["grouping"])
		:attr("data-filters", filterStr)
		--:cssText("position:relative; font-size:smaller; width:100%; height:100%; box-sizing:border-box; border:1px solid #7E5900; border-radius:7px; overflow:hidden")
		-- Header "row"
		:tag("div")
			:addClass("loot-container-header")
			--:cssText("text-align:center; line-height:22px; border-bottom:1px solid #7E5900;")
			:wikitext(header)
			:tag("div")
				:addClass("loot-container-header-arrow")
				--:cssText("position:absolute; left:6px; top:0px; cursor:pointer; transform:rotate(-90deg); transition:transform 0.5s ease 0s;")
				:wikitext("▼")
			:done()
			:tag("div")
				:addClass("loot-container-header-mapnode")
				--:cssText("position:absolute; right:6px; top:0px; cursor:pointer; font-weight:bold")
				:wikitext(mapNodeString)
			:done()
		:done()
		-- Detail / icon "row"
		:wikitext(tooltip)
	
	local items = tbl:tag("div"):cssText("padding:0.2em 0.4em;")
			
	-- Add fixed items row
	if (not isNilOrEmpty(container["inventory"]) and not isNilOrEmpty(container["inventory_qty"])) then
		
		local fixedInventoryItems = container["inventory_items"]
		local fixedItems = items:wikitext("Fixed loot:"):tag("ul")
		
		-- Loop over items, adding a li element for each one
		for i = 1, #fixedInventoryItems do
			
			-- Create list item
			local li = mw.html.create("li")
				:addClass("loot-container-fixed-item")
				:attr("data-filters", fixedInventoryItems[i].filters)
			
			-- Only include quantity if it's > 1
			if (tonumber(fixedInventoryItems[i].count) > 1) then
				li:wikitext(fixedInventoryItems[i].count, "x ")
			end
			
			-- Add link
			if (fixedInventoryItems[i].page ~= nil) then
				li:wikitext("[[", fixedInventoryItems[i].page, "|", fixedInventoryItems[i].name, "]]")
			else
				li:wikitext("[[", fixedInventoryItems[i].name, "]]")
			end
			
			-- Add list item to list
			fixedItems:node(li)
		end
	end
	
	-- Add random items row
	if (not isNilOrEmpty(container["lootlist"])) then
		
		local containerID = ""
		
		if (container["guid"] ~= nil) then
			containerID = string.sub(container["guid"], 1, 8)
		elseif (container["internalname"] ~= nil) then
			containerID = container["internalname"]
		else
			containerID = tostring(math.random(0, 1000000))
		end
		
		-- Create row, cell, and open div
		-- The dropdown is added in JavaScript
		local randomItems = items:wikitext("Random loot:<sup>[[Random loot|?]]</sup>")
			:tag("div"):addClass("lootlist-results"):attr("id", "lootlist-results-"..containerID):css("column-count", "2")
			
		-- Loop over all 20 day_x_loot tables
		for i = 1, 20 do
			
			local loot = container["day_"..i.."_loot"]
			
			-- Create colitem/ul containing all the items on a specific day
			local dayLoot = randomItems:tag("div")
				:addClass("colitem")
				:addClass("lootlist-results-day")
				:attr("data-day", tostring(i))
				:tag("span")
					:wikitext(common.stringAddOrdinal(i), ":")
				:done():tag("ul")
			
			-- Create a list item for each loot item
			for _, item in ipairs(loot) do
				
				local li = mw.html.create("li"):addClass("lootlist-results-item")
				
				-- Add filter strings on an item level
				if (not isNilOrEmpty(item.filters)) then
					li:attr("data-filters", item.filters)
				end
				
				-- Quantity - Only include if > 1
				if (item.count > 1) then
					li:wikitext(item.count, "x ")
				end
				
				-- Link (add link label if page name exists)
				if (item.page ~= nil) then
					li:wikitext("[[", item.page, "|", item.name, "]]")
				else
					li:wikitext("[[", item.name, "]]")
				end
				
				-- Add list item to list
				dayLoot:node(li)
			end
		end
		
	end
	
	return tostring(tbl)
end

-- Build the table used to output results of a Cargo container item query
function BuildContainerItemQueryTable(containers)
	
	function compareByLocation(a, b)
		return a.location < b.location
	end
	
	-- Sort the containers by location
	table.sort(containers, compareByLocation)
	
	-- Start building the HTML
	local tbl = mw.html.create("table")
	tbl:addClass("wikitable sortable")
	tbl:css("text-align", "center")
	
	local highestQty = 0
	
	-- Get the highest quantity
	-- Used to determine whether to use a dot (if all 1)
	-- Used to determine the column width
	for i = 1, #containers do
		for day = 0, 20 do
			if (containers[i].quantity[day] ~= nil and
				containers[i].quantity[day] > highestQty) then
				highestQty = containers[i].quantity[day]
				break
			end
		end
	end
	
	local allSingleQty = (highestQty == 1)
	local qtyColumnWidth = 3.32

	-- Table header row
	local head = tbl:tag("tr")
	head:node("<th style=\"min-width:230px\" rowspan=\"2\">Location</th>")
	head:node("<th rowspan=\"2\">Container</th>")
	head:node("<th style=\"min-width:"..tostring(qtyColumnWidth).."em\" rowspan=\"2\">Fixed</th>")
	head:node("<th colspan=\"20\" style=\"text-align:left\">Random loot (amount on each day of month)<sup>[[Random loot|?]]</sup></th>")
	head = tbl:tag("tr")
	
	for i = 1, 20 do
		head:tag("th")
			:css("min-width", tostring(qtyColumnWidth).."em")
			:wikitext(tostring(i))
	end
	
	local lastLocation
	
	-- Build container rows
	for i = 1, #containers do
		
		-- Start of container row
		local row = tbl:tag("tr")
		
		-- If this container is in a new location to the previous row start a new rowspan
		if (containers[i].location ~= lastLocation) then
			
			lastLocation = containers[i].location
			
			local rowspan = nil
		
			-- Count the number of other containers in this location
			for j = i + 1, #containers do
				if (containers[j].location == lastLocation) then
					if (rowspan == nil) then rowspan = 1 end
					rowspan = rowspan + 1
				else
					-- We can ignore containers after since they won't be the same location
					-- (the containers were sorted by location earlier, and are contigious)
					break
				end
			end
			
			-- Location cell
			row:tag("td")
			   :attr("rowspan", rowspan)
			   :css("text-align", "left")
			   :wikitext("[["..containers[i].location.."]]")
			
		end
		   
		-- Container cell, link to Loot page
		row:tag("td")
		   :cssText("white-space:nowrap; text-align:left")
		   :wikitext("[["..containers[i].page.."#"..containers[i].guid.."|"..containers[i].name.."]]")
		
		-- Finally, the loot cells
		for day = 0, 20 do
			local text = containers[i].quantity[day] ~= nil and (allSingleQty == true and "[[File:Point-icon.png|link=]]" or tostring(containers[i].quantity[day])) or nil
			row:tag("td"):wikitext(text)
		end
	end
	
	return tostring(tbl)
end

-- Build up the icon row of the container
function BuildContainerIconRow(container)
	
	local buffer = { "" }
	
	-- Container contains fixed loot
	if (not isNilOrEmpty(container["inventory"])) then
		table.insert(buffer, "&#x1F4CC; ")
	end
	
	if (not isNilOrEmpty(container["lootlist"])) then
		table.insert(buffer, "&#x1F3B2; ([[Loot lists#")
		table.insert(buffer, container["lootlist"])
		table.insert(buffer, "|")
		table.insert(buffer, container["lootlist"])
		table.insert(buffer, "]]) ")
	end
	
	-- Locked
	if (container["locked"] == "yes") then
									 
		table.insert(buffer, "[[File:CUR_locked_noarrow.png|x20px|middle|Locked|link=]] (")
		
		local keyLink = nil;
		
		-- Key present
		if (not isNilOrEmpty(container["key_item"])) then
			keyLink = "[["..container["key_item"].."|Key]]"
		end
		
		-- Key required
		if (container["key_required"] == "yes") then
			table.insert(buffer, keyLink)
		
		-- Key not required, or not present
		else
			table.insert(buffer, container["lock_difficulty"])
			
			if (keyLink ~= nil) then
				table.insert(buffer, ", ")
				table.insert(buffer, keyLink)
			end
		end
		
		table.insert(buffer, ") ")
	end
	
	-- Trapped
	if (container["trapped"] == "yes" and not isNilOrEmpty(container["trap_difficulty"])) then
		table.insert(buffer, "[[File:CUR_disarm_noarrow.png|x20px|middle|Trapped|link=]] (")
		table.insert(buffer, container["trap_difficulty"])
		table.insert(buffer, ") ")
	end
	
	-- Hidden
	if (container["hidden"] == "yes" and not isNilOrEmpty(container["detect_difficulty"])) then
		table.insert(buffer, "[[File:CUR_noLOS_noarrow.png|x20px|middle|Hidden|link=]] (")
		table.insert(buffer, container["detect_difficulty"])
		table.insert(buffer, ") ")
	end
	
	-- Stealing
	if (container["steal_faction"] ~= nil and string.lower(container["steal_rep"]) ~= "none") then
		table.insert(buffer, "[[File:CUR_steal_noarrow.png|x20px|middle|Stealing|link=]] (<i>")
		table.insert(buffer, container["steal_faction"])
		table.insert(buffer, "</i>)")
	end
	
	return table.concat(buffer)
end

-- Build up the icon grid of the container
function BuildContainerIconGrid(container)
	
	local grid = mw.html.create("div")
		:cssText("display:grid; grid-template-columns:24px auto; grid-auto-rows:minmax(2em, min-content); align-items:center; font-size:smaller")
	
	-- Contains fixed loot
	if (not isNilOrEmpty(container["inventory"])) then
		grid:tag("div"):wikitext("&#x1F4CC;"):cssText("font-size:16px; line-height:16px; align-self:center")
		grid:tag("div"):wikitext("Fixed loot")
	end
	
	-- Contains random loot
	if (not isNilOrEmpty(container["lootlist"])) then
		grid:tag("div"):wikitext("&#x1F3B2;"):cssText("font-size:16px; line-height:16px; align-self:center")
		grid:tag("div"):wikitext("Random loot<br/>([[Loot lists#"..container["lootlist"].."|"..container["lootlist"].."]])")
	end
	
	-- Locked
	if (container["locked"] == "yes") then
		
		local buffer = { "Locked ("}
		local keyLink = nil;
		
		-- Key present
		if (not isNilOrEmpty(container["key_item"])) then
			keyLink = "[["..container["key_item"].."]]"
		end
		
		-- Key required (i.e. cannot open with Mechanics)
		if (container["key_required"] == "yes" and keyLink ~= nil) then
			table.insert(buffer, keyLink)
		
		-- Key not required, or not present
		else
			table.insert(buffer, "Difficulty ")
			table.insert(buffer, container["lock_difficulty"])
			
			if (keyLink ~= nil) then
				table.insert(buffer, ", ")
				table.insert(buffer, keyLink)
			end
		end
		
		table.insert(buffer, ") ")
		
		grid:tag("div"):wikitext("[[File:CUR_locked_noarrow.png|20px|middle|Locked|link=]]")
		               :cssText("align-self:center")
		grid:tag("div"):wikitext(table.concat(buffer))
	end
	
	-- Trapped
	if (container["trapped"] == "yes" and not isNilOrEmpty(container["trap_difficulty"])) then
		grid:tag("div"):wikitext("[[File:CUR_disarm_noarrow.png|x20px|middle|Trapped|link=]]")
		               :cssText("align-self:center")
		grid:tag("div"):wikitext("Trapped (Difficulty "..container["trap_difficulty"]..")")
	end
	
	-- Hidden
	if (container["hidden"] == "yes" and not isNilOrEmpty(container["detect_difficulty"])) then
		grid:tag("div"):wikitext("[[File:CUR_noLOS_noarrow.png|x20px|middle|Hidden|link=]]")
					   :cssText("align-self:center")
		grid:tag("div"):wikitext("Hidden (Difficulty "..container["detect_difficulty"]..")")
	end
	
	-- Stealing
	if (container["steal_faction"] ~= nil and string.lower(container["steal_rep"]) ~= "none") then
		grid:tag("div"):wikitext("[[File:CUR_steal_noarrow.png|x20px|middle|Stealing|link=]]")
		grid:tag("div"):wikitext("Stealing (<i>"..container["steal_faction"].."</i>)")
	end
	
	return tostring(grid)
end

-- Build up the tooltip in its entirety, mostly building the content string
-- We use a table to concatenate strings here instead of using .. to save us a fair bit of CPU time and memory.
-- It may not be as pretty, but it's tested to be much faster with this many strings
function BuildContainerTooltip(frame, container, wrapper, display)
	
	local buffer = { "" }
	
	-- Container contains fixed loot
	if (not isNilOrEmpty(container["inventory"])) then
		table.insert(buffer, "<span style=\"width:24px; display:inline-block\">&#x1F4CC;</span>Contains <b>fixed loot</b> always present on opening\n")
	end
	
	if (not isNilOrEmpty(container["lootlist"])) then
		table.insert(buffer, "<span style=\"width:24px; display:inline-block\">&#x1F3B2;</span>Contains <b>random loot</b> — One of the following 20 sets of items is present depending on the day of the month\n")
	end
	
	table.insert(buffer, "<hr style=\"margin-bottom:10px\"/>\r\n")
	
	-- Container is locked
	if (container["locked"] == "yes") then
		
		local lockDifficulty = tonumber(container["lock_difficulty"])
		local lockKey = container["key_item"]
		local isKeyRequired = container["key_required"] == "yes"
		
		table.insert(buffer, "[[File:CUR locked noarrow.png|x16px|middle]]&nbsp;&nbsp;Container is <b>locked</b>")
		
		-- Key not required, list mechanics requirements
		if (isKeyRequired == false) then
			table.insert(buffer, " (Difficulty ")
			table.insert(buffer, tostring(lockDifficulty))
			table.insert(buffer, ")\n* Requires one of the following:\n** ")
			table.insert(buffer, tostring(lockDifficulty - 1))
			table.insert(buffer, " [[Mechanics]], 3 Lockpicks\n** ")
			table.insert(buffer, tostring(lockDifficulty))
			table.insert(buffer, " [[Mechanics]], 1 Lockpick\n** ")
			table.insert(buffer, tostring(lockDifficulty + 1))
			table.insert(buffer, " [[Mechanics]]\n")
			
			-- Key for this container is present, but not required
			if (not isNilOrEmpty(lockKey)) then
				table.insert(buffer, "* Can also be opened with the [[")
				table.insert(buffer, tostring(lockKey))
				table.insert(buffer, "]]\n")
			end
		end
		
		-- Key required
		if (isKeyRequired == true and lockKey ~= nil) then
			table.insert(buffer, "\n* Requires the [[")
			table.insert(buffer, lockKey)
			table.insert(buffer, "]] (cannot be lockpicked)\n")
		end
		
		table.insert(buffer, "\n")
	end
	
	-- Container is trapped
	if (container["trapped"] == "yes" and not isNilOrEmpty(container["trap_difficulty"])) then
		
		local trapItem = container["trap_item"]
		table.insert(buffer, "[[File:CUR disarm noarrow.png|x16px|sub]]&nbsp;&nbsp;Container is <b>trapped</b> (Difficulty ")
		table.insert(buffer, container["trap_difficulty"])
		table.insert(buffer, ")<br/>\n* Requires at least [[Mechanics]] ")
		table.insert(buffer, container["trap_difficulty"])
		table.insert(buffer, " to disable")
		
		if (container["trap_item"] ~= nil) then
			table.insert(buffer, "\n* Disabling gives a [[")
			table.insert(buffer, container["trap_item"])
			table.insert(buffer, "]]")
		end
		
		table.insert(buffer, "\n")
	end
	
	-- Container is hidden
	if (container["hidden"] == "yes" and container["detect_difficulty"] ~= nil) then
		
		local detectDifficulty = tonumber(container["detect_difficulty"])
		
		-- Trap is hidden
		table.insert(buffer, "[[File:CUR noLOS noarrow.png|x16px|sub]]&nbsp;&nbsp;")
		table.insert(buffer, common.ternary(container["trapped"] == "yes" and container["trap_difficulty"] ~= nil, "Trap", "Container"))
		table.insert(buffer, " is <b>hidden</b> (Difficulty ")
		table.insert(buffer, detectDifficulty)
		table.insert(buffer, ")<br/>\n* Requires at least [[Mechanics]] ")
		table.insert(buffer, detectDifficulty)
		table.insert(buffer, " (in Scouting Mode) to be detected at a 1 meter distance")
		
		table.insert(buffer, "\n")
	end
	
	-- Container is owned
	if (not isNilOrEmpty(container["steal_faction"])) then
		
		table.insert(buffer, "[[File:CUR steal noarrow.png|x16px|sub]]&nbsp;&nbsp;Container is <b>owned</b> by [[")
		table.insert(buffer, container["steal_faction"])
		table.insert(buffer, "]]\n* Being caught stealing incurs a ")
		
		local rep = string.lower(container["steal_rep"])
		if		(rep == "veryminor")	then rep = "Slight (1)"
		elseif	(rep == "minor")		then rep = "Minor (2)"
		elseif	(rep == "average")		then rep = "Moderate (4)"
		elseif	(rep == "major")		then rep = "Major (6)"
		elseif	(rep == "verymajor")	then rep = "Extraordinary (8)"
		end
		
		table.insert(buffer, rep)
		table.insert(buffer, " reputation loss with this faction\n")
		
		if (container["attack_thief"] == "yes") then
			table.insert(buffer, "\n* Nearby faction members become hostile")
		end
		if (container["ally_attack_thief"] == "yes") then
			table.insert(buffer, "\n* Nearby allied faction members become hostile")
		end
	end
	
	-- Add technical information
	local technical = { }
	
	if (not isNilOrEmpty("internalname")) then
		table.insert(technical, container["internalname"])
	end
	if (not isNilOrEmpty("lootlist")) then
		table.insert(technical, container["lootlist"])
	end
	
	table.insert(buffer, "<div style=\"text-align:right; color:grey; font-size:smaller; padding-top: 4px\">")
	table.insert(buffer, table.concat(technical, "<br/>"))
	table.insert(buffer, "</div>")
	
	-- Finally, actually construct the tooltip
	
	local tooltipArgs =
	{
		["clamp-viewport"] = "push"
	}
	
	if (display == "box") then
		tooltipArgs["follow-cursor"] = "x"
		tooltipArgs["update-event"] = "mousemove"
		tooltipArgs["container-anchor"] = "bottom-left"
		tooltipArgs["style"] = "width:100%; line-height:26px; vertical-align: middle; text-align:center; border-bottom:1px solid #7E5900; background:rgba(0,0,0,0.5);"
	elseif (display == "row") then
		tooltipArgs["anchor"] = "right"
		tooltipArgs["style"] = "display:block"
	end
	
	tooltipArgs["container"] = wrapper;
	tooltipArgs["content"] = table.concat(buffer)
	
	-- Expand the template Tooltip_poe2 using the above arguments. This returns wikitext for us to use in the table
	return frame:expandTemplate{ title = "Tooltip_poe2", args = tooltipArgs };
end

-- Build up a filter string for the container
function BuildContainerFilterString(container)
	local buffer = { }
	
	if (container["locked"] == "yes") then
		table.insert(buffer, "locked")
	end
	
	if (not isNilOrEmpty(container["key_item"])) then
		table.insert(buffer, "locked_key")
	end
	
	if (container["trapped"] == "yes") then
		table.insert(buffer, "trapped")
	end
	
	if (container["hidden"] == "yes") then
		table.insert(buffer, "hidden")
	end
	
	if (not isNilOrEmpty(container["steal_faction"])) then
		table.insert(buffer, "stealing")
	end
	
	if (not isNilOrEmpty(container["inventory"])) then
		table.insert(buffer, "fixed_loot")
	end
	
	if (not isNilOrEmpty(container["lootlist"])) then
		table.insert(buffer, "random_loot")
	end
	
	return table.concat(buffer, ";")
end

-- Fetch the filter strings given input random loot, adding the filter strings to the table
-- (i.e. does not create a new table, does not return anything)
-- Loot should be a table of tables, containing { id, name, page, count } though only name is required.
-- Loot should ideally contain distinct values
function GetLootFilterStrings(loot)

	for i = 1, #loot do
		loot.filters = GetItemData(loot[i].id).filters or ""
	end
end

-- Same as above, but determines the filter strings from a series of cargo queries
function GetLootFilterStringsCargo(loot)
	
	local query = 
	{
		-- Unfortunately we can't use CONCAT_WS since it's not an allowed SQL keyword on this wiki
		{ name = "Item_poe1", fields = "name, LOWER(item_category) = a, LOWER(item_type) = b" },
		{ name = "Shield_poe1", fields = "name, CONCAT('shield') = a, LOWER(shield_type) = b" },
		{ name = "Armor_poe1", fields = "name, CONCAT('armor') = a, LOWER(armor_type) = b" },
		{ name = "Weapon_poe1", fields = "name, CONCAT('weapon') = a, LOWER(weapon_type) = b" }
	}
	
	-- Create query string with loot items, prepend with "name IN ("," append with "")"
	local searchStr = "name IN (\""..table.concat(common.tableSelectValues(loot, "page"), "\", \"").."\")"

	local args =
	{
		where = searchStr,
		groupBy = "name" -- <- Make distinct
	}
	
	local resultsCount = 0
	
	-- Do cargo query in order of most-likely-to-return-results,
	-- and check to see if we have all the results so we don't have to perform unnecessary queries
	for i = 1, #query do
		
    	local results = cargo.query(query[i].name, query[i].fields, args)
    	resultsCount = resultsCount + #results
    	
    	-- Add filters from results to the associated input loot item
    	for r = 1, #results do
    		local lootItem = common.tableGetTableWithKeyOfValue(loot, "page", results[r].name, true)
			local str = ""
			
			-- Add fields "a", "b"
			if (not isNilOrEmpty(results[r].a)) then str = str..results[r].a..";" end
			if (not isNilOrEmpty(results[r].b)) then str = str..results[r].b..";" end
    				
			-- Strip trailing semicolon
			if (stringEndsWith(str, ";")) then
				str = string.sub(str, 1, #str - 1)
			end
			
    		lootItem.filters = str
    	end
    	
    	if (resultsCount == #loot) then
    		break
    	end
    end
end

-- OBSOLETE
-- Same as the above function, but changed to support fixed loot items
-- (making a generic function would be too much effort)
-- Changes from the above function:
	-- The input loot parameter should be an array of item names
	-- A query is performed based on the item name, not the internalname
	-- Queries the "is_unique" field of items, as unique items are (for the most part) only found as fixed items
	-- Returns an array of filter strings
function GetFixedLootFilterStrings(loot)
	
	local query = 
	{
		-- Unfortunately we can't use CONCAT_WS since it's not an allowed SQL keyword on this wiki
		{ name = "Item_poe1", fields = "name, LOWER(item_category) = a, LOWER(item_type) = b, is_unique" },
		{ name = "Shield_poe1", fields = "name, CONCAT('shield') = a, LOWER(shield_type) = b, is_unique" },
		{ name = "Armor_poe1", fields = "name, CONCAT('armor') = a, LOWER(armor_type) = b, is_unique" },
		{ name = "Weapon_poe1", fields = "name, CONCAT('weapon') = a, LOWER(weapon_type) = b, is_unique" }
	}
	
	-- Create query string with loot items, prepend with "name IN ("," append with "")"
	local searchStr = "name IN (\""..table.concat(loot, "\", \"").."\")"

	local args =
	{
		where = searchStr,
		groupBy = "name" -- <- Make distinct
	}
	
	local resultsCount = 0
	local filters = {}
	
	-- Do cargo query in order of most-likely-to-return-results,
	-- and check to see if we have all the results so we don't have to perform unnecessary queries
	for i = 1, #query do
		
    	local results = cargo.query(query[i].name, query[i].fields, args)
    	
    	-- Add filters from the results to the output at the associated index
    	for r = 1, #results do
    		
    		-- Loop over input loot items to find the item associated with this result
    		-- (the results are almost always in a different order than the input loot)
    		for k = 1, #loot do
    			
    			if (results[r].name == loot[k]) then
    				
    				filters[k] = ""
    				
					-- Add fields "a", "b"
    				if (not isNilOrEmpty(results[r].a)) then filters[k] = filters[k]..results[r].a..";" end
    				if (not isNilOrEmpty(results[r].b)) then filters[k] = filters[k]..results[r].b..";" end
    				
    				-- Add "unique_item" if the item is unique
    				if (results[r].is_unique == "yes") then
    					filters[k] = filters[k].."unique_item"
    				end
    				
    				-- Strip trailing semicolon
    				if (stringEndsWith(filters[k], ";")) then
    					filters[k] = string.sub(filters[k], 1, #filters[k] - 1)
    				end
    				
    				resultsCount = resultsCount + 1
    			end
    			
    		end
    		
    	end
    	
    	-- If the amount of processed results matches the input loot, return
    	if (resultsCount == #loot) then
    		break
    	end
    end
    
    return filters
end

-- Lootlist functions

-- Returns a collection of items (an array of item ID strings) by evaluating the loot list
-- Must be passed the lootlist to evaluate (directly from the cargo query). The rolls will be made based on a set of initialization parameters for Xorshift (both set in the above #invoke'd functions)
-- The order of the returned items is determined by the evaluation order. The in-game order is sorted based on the item type as well as its value, but to avoid slowing down evaluation we won't bother doing this.
-- The recursionDepth parameter is for logging purposes only
function EvaluateLootList(lootlist, recursionDepth)
	
	local items = { } -- The resulting items, an array with a table of { item, page } for each item
	local done = false; -- True when we've added an item from this list, signalling that we should add no more
	local roll = nil
	
	if (lootlist == nil or lootlist.items == nil) then
		--mw.log("Cannot evaluate an empty lootlist, returned nil")
		return items
	end
	
	if (recursionDepth == nil) then recursionDepth = 0 end
	local prefix = string.rep("\t", recursionDepth)
	
	-- This isn't required if we have predefined rolls, but IS if we instead use the RNG state
	local totalWeight = lootlist["total_weight"]
	
	--mw.log(prefix.."Total weight is "..totalWeight)
	
	-- Make roll from 0 to totalWeight (exclusive) using Xorshift state.
	-- If the state values are correct, this will mimic the exact rolls as made in game
	roll = Xorshift128_NextUIntMax(totalWeight)
	
	local cumulativeWeight = 0; -- Used for weighted random selection
	--mw.log(prefix.."### "..lootlist[1].lootlist.." - Rolled " .. roll.." of d"..totalWeight.." ###")
	
	-- Loop over the LootItems in the lootlist
	for _, lootItem in ipairs(lootlist.items) do
		
		--mw.log(prefix.."["..lootItem.position.."] "..common.default(lootItem.item, "<empty>").." (Weight: "..lootItem.weight..", Always: "..lootItem.always..", Count: "..tostring(lootItem.count)..")")
    	
    	-- Whether we should add this to the output items
    	local shouldAdd = false
    	
    	-- Always add this item
    	if (lootItem.always == true) then
    		shouldAdd = true
    		--mw.log(prefix.."\tAdding this item (always)")
    	else
    		-- Add this LootItem's weight to the cumulative weight
    		cumulativeWeight = cumulativeWeight + lootItem.weight
    		--mw.log(prefix.."\tCumulative weight is now "..cumulativeWeight)
    		
    		-- If we've reached the value defined in the roll, pick this one!
    		if (roll < cumulativeWeight and done ~= true) then
    			--mw.log(prefix.."\tAdding this item ("..roll.." is now < "..cumulativeWeight..")")
    			shouldAdd = true
    			done = true
    		end
    	end
    	
    	-- If the item is actually present (and isn't an empty value), and if we should add it
    	if (lootItem.id ~= nil and shouldAdd) then
    		
    		--mw.log(prefix.."\tEvaluating "..common.default(lootItem.item, "<empty>").." "..lootItem.count.." times")
    		
    		-- Repeat adding the item <count> times
    		for count = 1, tonumber(lootItem.count) do
    			
    			-- Recurse if the LootItem is a lootlist
    			if (lootItem.is_lootlist == true) then
    				
    				--mw.log(prefix.."\tLootItem is LootList! Begin recursion...")
    				
    				-- Evaluate child LootList with the next roll
    				local childLootList = GetLootlist(lootItem.id)
    				childItems = EvaluateLootList(childLootList, recursionDepth + 1)
    				
    				-- Add all resulting (child)items to the results
    				if (childItems ~= nil) then
    					for i=1, #childItems do
    						items[#items + 1] = childItems[i]
    					end
    				end
    				
    				--mw.log(prefix.."\tEnd recursion!")
    					
    			-- or just add the item if it's an item
    			else
    				--mw.log(prefix.."\tAdded 1x "..common.default(lootItem.item, "<empty>"))
    				items[#items + 1] = lootItem.id
    			end
    			
    		end -- end for
    		
    	end -- end if
    	
	end -- end for
	
	return items
end

-- "Stack" duplicate items, adding a count field and removing empty items.
-- Note that these won't be valid sized stacks (we don't store MaxStackSizes on the wiki)
-- ...we don't really need or want them to be since it's very unlikely that a container has multiple full stacks of something
-- Assumes the input is an array of item ID strings [ "id1", "id2", "id3" ]
-- Outputs a new array of items in the format { id, count }
function StackItems(items)
	local output = { }
	
	-- Loop through all the items in the passed table
	for _, item in ipairs(items) do
		
		-- Get the existing table with the same item ID from the output (if it exists)
		local outputItem = common.tableGetTableWithKeyOfValue(output, "id", item)
		
		-- Add 1 to the count if the output already contains an item with this ID
		if (outputItem ~= nil) then
			outputItem.count = outputItem.count + 1
			
		-- Otherwise create a new item object with { id, count } to add to the output
		elseif (item ~= nil and item ~= "") then
			outputItem = { ["id"] = item, ["count"] = 1 }
			table.insert(output, outputItem)
		end
	end
	
	return output
end

-- Sort items *in-place* in the same fashion as in game
-- Items can either be an array of ids or an array of item objects (containing the id)
function SortItems(items)
	table.sort(items, SortItem)
end

local EQUIPPABLES = { "armor", "accessory", "clothing", "weapon", "grimoire", "pet", "shield" }

-- Receives two arguments, returning true if the first argument should come first in the sorted array
-- As reference, in C# a sort function would return a negative integer if the first argument comes first, and a positive integer if the second argument comes first
function SortItem(a, b)
	
	-- a nil, b comes first
	if (a == nil)
		then return false
	end
	
	-- b nil, a comes first
	if (b == nil)
		then return true
	end
	
	-- Fetch item data required to sort
	local ad = GetItemData(a.id or a)
	local bd = GetItemData(b.id or b)
	
	-- Filter type, low-to-high (NONE = 0, WEAPONS = 1, ARMOR = 2, DEPRECATED_AMMO = 4, CLOTHING = 8, CONSUMABLES = 16, INGREDIENTS = 32, QUEST = 64, MISC = 128)
	-- If the difference between the filterTypes is:
		-- Negative - a comes first, return true
		-- Positive - b comes first, return false
		-- 0 - Move to next rule
	local flag1 = ad.filterType - bd.filterType
	if (flag1 ~= 0) then
		return flag1 < 0
	end
	
	-- Consumable type, low-to-high, only if both items are consumable (Ingestible = 0, Scroll = 1, Figurine = 2, Trap = 3, Potion = 4, Drug = 5, Count = 6, None = 7)
	-- If the difference between the consumable types is:
		-- Negative - a comes first, return true
		-- Positive - b comes first, return false
		-- 0 - Move to next rule
	if (ad.consumableType ~= nil and bd.consumableType ~= nil) then
		local flag2 = ad.consumableType - bd.consumableType
		if (flag2 ~= 0) then
			return flag2 < 0
		end
	end
	
	-- If equippable, secondary weapon slot only items (i.e. shields) are sorted after other equippables
	if (IsItemEquippable(ad) and IsItemEquippable(bd)) then

		-- The in-game equivalent is a.SecondaryWeaponSlot && !a.PrimaryWeaponSlot,
		-- though we don't need to store this since we know only shields are secondary slot only
		local isAShield = string.find(ad.filters, "shield", 1, true)
		local isBShield = string.find(bd.filters, "shield", 1, true)
		
		-- If a is a shield and b isn't a shield, b comes first (return false)
		if (isAShield and not isBShield) then
			return false
		end
			
		-- If b is a shield and a isn't a shield, a comes first (return true)
		if (isBShield and not isAShield) then
			return true
		end
			
		-- If both are shields or both aren't shields, continue below
		
	end
	
	-- Value, high-to-low
	-- If the difference between b and a is:
		-- Negative (a is higher) - a comes first, return true
		-- Positive (b is higher) - b comes first, return false
		-- 0 - Continue below
	local flag3 = bd.value - ad.value
	if (flag3 ~= 0) then
		return flag3 < 0
	end
	
	return ad.name < bd.name
end

function IsItemEquippable(itemData)
	
	for i = 1, #EQUIPPABLES do
		if (string.find(itemData.filters, EQUIPPABLES[i], 1, true)) then
			return true	
		end
	end
	
	return false
end

-- Adds name, page, and filters to items from the data module
-- Assumes items is an array of [ { id, count }, ... ]
function AddItemData(items)
	
	if (items == nil) then return end
	
	for i = 1, #items do
		
		local data = GetItemData(items[i].id)
		
		if (data == nil) then
			items[i].name = items[i].id
		else
			items[i].name = data.name;
			items[i].page = data.page;
			items[i].filters = data.filters
		end
	end
	
end

-- Returns a table that contains all items in a specific lootlist
function GetLootlist(name)
	
	-- Return nil (don't throw error) if name isn't passed
	if (isNilOrEmpty(name)) then
		return nil
	end
	
	-- Ensure the data module is loaded
	if (checkDataLoaded() == false) then
		data = mw.loadData("Module:Lootlist/data")
	end
	
	local lootlist = data.lootlists[name]
    
    -- If a lootlist with this name wasn't found, throw error
    if (lootlist == nil) then
    	error("A lootlist with the name "..name.." was not found, or returned no results"); return
    end
    
    return lootlist
end

-- Same as above, but queries the Lootitem_poe1 cargo table instead
function GetLootlistCargo(name)
	
	-- Return nil (don't throw error) if name isn't passed
	if (isNilOrEmpty(name)) then
		return nil
	end
	
	local tables = "Lootitem_poe1"
	local fields = "lootlist, position, item, page, is_lootlist, count, weight, always"
	local args =
	{
		where   = "lootlist = '" .. name .. "'",
		orderBy = "position",
		limit   = 5000,
	}
	
	-- Do cargo query
    local query = cargo.query(tables, fields, args)
    
    -- If no items are found, throw error
    if (next(query) == nil) then
    	error("A lootlist with the name "..name.." was not found, or returned no results"); return
    end
    
    return query
end

-- Get data for an item with a specific ID from the data module
-- The data is a table with the fields { id, name, page, filters, etc }
-- Keep in mind that the data returned will be READ ONlY.
function GetItemData(id)

	-- Ensure the data module is loaded
	if (checkDataLoaded() == false) then
		data = mw.loadData("Module:Lootlist/data")
	end
	
	local item = data.items[id:lower()]
	
	-- If an item with this ID wasn't found, throw error
    if (item == nil) then
    	return nil
    	--error("An item with the ID "..id.." was not found"); return
    end
	
	return item
end

function GetItemDataByName(name)
	
	-- Ensure the data module is loaded
	if (checkDataLoaded() == false) then
		data = mw.loadData("Module:Lootlist/data")
	end
	
	local item = nil
	local id = nil
	
	for k,v in pairs(data.items) do
		if (v.name == name) then
			item = v
			id = k
			break
		end
	end
	
	-- If an item with this name wasn't found, throw error
    if (item == nil) then
    	error("An item with the name "..name.." was not found"); return
    end
    
    -- Remake to enable read-write
	local itemRw =
	{
		["id"] = id,
		["name"] = item.name,
		["page"] = item.page or nil,
		["filters"] = item.filters
	}
	
	return itemRw
end

--- Calculates the total weight of the lootlist, a direct output from a Lootitem_poe1 cargo query
function GetTotalWeight(lootlist)
	local totalWeight = 0
	
	for _, lootItem  in next, lootlist do
		if (lootItem.always ~= "1") then
			totalWeight = totalWeight + lootItem.weight
		end
	end
	
	return totalWeight
end

-- Check to see if the data module is loaded
function checkDataLoaded()
	if (data == nil) then
		return false
	end
	
	return true
end

-- Game helper functions

-- Generates the seed in the same way the game does. Expects the xz position of the container and the day of the month (1-20)
-- (int)((double)loot.transform.position.x + (double)loot.transform.position.z) * GameState.s_playerCharacter.name.GetHashCode() + day)
function CalculateSeedForContainer(x, z, day)
	if (tonumber(x) == nil or tonumber(z) == nil or tonumber(day) == nil) then
		error("One or more of the parameters x, z, day for CalculateSeedForContainer is nil or NaN")
		return nil
	end
	
	-- Truncate to strip decimal portion
	local t, _ = math.modf(x + z)
	
	-- Calculate the seed
	local long = t * 1413436758 + day
	
	-- Replicate C# "long" to "int" casting, converting the signed 64-bit value (default in Lua) to a signed 32-bit integer
	-- This effectively removes the upper 32 bits, but preserves the sign
	return (long + 2^31) % 2^32 - 2^31
	
	-- A different way to do the same using the bit32 library
	--[[
	if (a < 0) then
		b = -bit32.band(0xFFFFFFFF, -a)
	else
		b = bit32.band(0xFFFFFFFF, a)
	end
	]]--
end

-- World position to map position
-- The world background quad is placed on the XZ-plane with the camera above facing down (-Y) onto it, however the camera is actually rotated by 180 + 45 degrees on the Y axis (and 40 degrees on the X axis) such that:
	-- The world +X axis is towards the bottom left of the screen (as opposed to right of the screen)
	-- The world -X axis is towards the top right of the screen (as opposed to left of the screen)
	-- The world +Z axis is towards the bottom right of the screen (as opposed to the top of the screen)
	-- The world -Z axis is towards the top left of the screen (as opposed to the bottom of the screen)
-- This helper function will convert a world xz position to a map position by rotating it, then applying a scale to a target size.
-- Vector2 coordinates are referred to as XZ instead of XY since the map doesn't make use of the Y axis
-- Parameters:
	-- worldX, worldZ: The input world position to be converted
	-- worldBotLeftX, worldBotLeftZ, worldTopRightX, worldTopRightZ: The bottom left and top right positions of the scene background quad. This is LevelInfo.m_backgroundQuadVertex00 and LevelInfo.m_backgroundQuadVertex11 respectively
	-- mapWidth, mapHeight: The target size of the map. The aspect ratio should match the input coordinates, or will be stretched dispropotionately. The target size should typically be 1084 x 610 to match the in-game map size, but can be any increment larger or smaller.
function WorldToMapPosition(worldX, worldZ,
							worldBotLeftX, worldBotLeftZ,
							worldTopRightX, worldTopRightZ,
							mapWidth, mapHeight)
	
	-- Rotate the world background corners -45 degrees (Y) to convert it to the camera axis
	local blx, bly, blz = quaternionMultVector(0, -0.3826835, 0, 0.9238795, worldBotLeftX, 0, worldBotLeftZ)
	local trx, try, trz = quaternionMultVector(0, -0.3826835, 0, 0.9238795, worldTopRightX, 0, worldTopRightZ)
	
	-- Do the same with the input world position
	local wx, wy, wz = quaternionMultVector(0, -0.3826835, 0, 0.9238795, worldX, 0, worldZ)
	
	-- Scale the rotated position from world units to map units
	local mx = scale(wx, blx, trx, 0, mapWidth)
	local my = scale(wz, trz, blz, 0, mapHeight)
	
	return mx, my
end

-- Random number generation functions
-- Integer RNG is 1-to-1 with Unity, while floating point has a slight variation on the precision

-- Xorshift128 state (globals)
-- x is seed (only if positive), yzw are generated based on the seed.
x = 0; y = 0; z = 0; w = 0
MT19937 = 1812433253
INT32_MIN  = -2147483648 -- 0x80000000
INT32_MAX  = 2147483647  -- 0x7FFFFFFF
UINT32_MAX = 4294967295  -- 0xFFFFFFFF

function p.Xorshift128_Tests()
	local data = mw.loadData("Module:Lootlist/data")
	
	Xorshift128_InitSeed(1234)
	
	-- Print some integers
	for i = 1, 5 do
		mw.log(Xorshift128_Next())
	end
	
	-- Print some ranged integers
	-- 0 to max
	p.Xorshift128_IntegerTests(0, INT32_MAX)
	-- 0 to min
	p.Xorshift128_IntegerTests(0, INT32_MIN)
	-- Min to max
	p.Xorshift128_IntegerTests(INT32_MIN, INT32_MAX)
	-- Max to min 
	p.Xorshift128_IntegerTests(INT32_MAX, INT32_MIN)
	-- Min to min (error)
	p.Xorshift128_IntegerTests(INT32_MIN, INT32_MIN)
	
	-- Print some floating pointss
	for i = 1, 5 do
		mw.log(Xorshift128_NextFloat())
	end
	
	-- Print some ranged floating points
	for i = 1, 5 do
		mw.log(Xorshift128_NextFloat())
	end
	
	--[[
	local limit = 0
	local equal = 0
	local notEqual = 0
	
	-- Compare generated states with pregenerated states
	for k, v in pairs(data.states) do
		limit = limit + 1
		
		Xorshift128_InitSeed(k)
		
		-- Confirm that state is equal to pregen
		if (x ~= v[1] or y ~= v[2] or z ~= v[3] or w ~= v[4]) then
			notEqual = notEqual + 1
		else
			equal = equal + 1
		end
		
		mw.log("Seed: "..k)
		mw.log("Pregen:".."x="..v[1]..", y="..v[2]..", z="..v[3]..", w="..v[4])
		mw.log("Newgen:".."x="..x..", y="..y..", z="..z..", w="..w)
		
		if (limit == 50) then
			break
		end
	end
	
	mw.log("Pregenerated vs generated states\n\tEqual: "..equal.."\n\tNot equal: "..notEqual)
	]]--
end

function p.Xorshift128_IntegerTests(min, max)
	mw.log("--- "..min.." to "..max.." ---")
	for i = 1, 5 do
		mw.log(Xorshift128_NextIntRange(min, max))
	end
end

function p.Xorshift128_FloatingPointTests(min, max)
	mw.log("--- "..min.." to "..max.." ---")
	for i = 1, 5 do
		mw.log(Xorshift128_NextFloatRange(min, max))
	end
end

-- Initialize Xorshift using a signed integer seed, calculating the state values using the initialization method from Mersenne Twister (MT19937)
-- https://en.wikipedia.org/wiki/Mersenne_Twister#Initialization
function Xorshift128_InitSeed(seed)
	if (seed == nil or type(seed) ~= "number") then
		error("Seed parameter is not a number");
		return
	end
	
	-- Convert the input signed int to an unsigned int
	x = overflow32U(seed)
	
    -- Break the 1812433253 into two prime factors (1289 * 1406077 = 1812433253) and perform two multiplication operations with a bitwise right shift on each factor to convert the result into a unsigned 32-bit int
    -- This is the same as the following Lua operation (but much faster, given we know the prime factors in advance)
    	-- y = uintMultiply(MT19937, x) + 1
    -- and primarily C#'s:
    	-- y = (uint)(MT19937 * x + 1)
    y = bit32.rshift(1289 * bit32.rshift(1406077 * x, 0) + 1, 0)
    z = bit32.rshift(1289 * bit32.rshift(1406077 * y, 0) + 1, 0)
    w = bit32.rshift(1289 * bit32.rshift(1406077 * z, 0) + 1, 0)
end

-- Initialize Xorshift using a seed (supported via the lookup table Module:Lootlist/data)
-- NO LONGER USED - We can generate 1-to-1 integers now! :)
function Xorshift128_InitSeedPregen(seed)
	if (type(seed) ~= "number") then
		error("Seed parameter is not a number"); return
	elseif (data == nil or data.states == nil) then
		error("Pregenerated state data not loaded!"); return
	end
	
	local state = data.states[seed]
		
	if (state == nil or state[1] == nil or state[2] == nil or state[3] == nil or state[4] == nil) then
		error("Pregenerated states for seed "..seed.." in Modules:Lootlist/data not found or invalid"); return
	end
	
	Xorshift128_Init(state[1], state[2], state[3], state[4])
end

-- Initialize the entire Xorshift state explicitly
function Xorshift128_Init(x, y, z, w)
	_G.x = x; _G.y = y; _G.z = z; _G.w = w;
end

-- Generate a random unsigned 32-bit integer value (0 to 4,294,967,295)
-- This is the equivalent of a uint in C#.
function Xorshift128_Next()
	-- C equivalent: x ^ (x << 11)
	local t = bit32.bxor(x, bit32.lshift(x, 11))
	x = y; y = z; z = w
	-- C equivalent: w = w ^ (w >> 19) ^ t ^ (t >> 8)
	w = bit32.bxor(w, bit32.rshift(w, 19), t, bit32.rshift(t, 8))
	
	return w
end

-- Alias of the above
function Xorshift128_NextUInt()
	return XORShift128_Next()
end

-- Generate random unsigned 32-bit integer value in the range 0 (inclusive) to max (exclusive)
function Xorshift128_NextUIntMax(max)
	if (max == 0) then
		return 0
	else
		return Xorshift128_Next() % max
	end
end

-- Generate random unsigned 32-bit integer value in the range min (inclusive) to max (exclusive)
function Xorshift128_NextUIntRange(min, max)
	if (max - min == 0) then
		return min
	else
		return min + Xorshift128_Next() % (max - min)
	end
end

-- Mod the result by int.MaxValue (2,147,483,647) to wrap the value to be in the positive 32-bit signed integer range.
function Xorshift128_NextInt()
	return Xorshift128_Next() % 2147483647
end

function Xorshift128_NextIntRange(min, max)
	
	if (max - min == 0) then
		return min
	end
	
	local r = Xorshift128_Next()
	
	if (max < min) then
		return min - math.fmod(r, (max - min))
	else
		return min + math.fmod(r, (max - min))
	end
	
end

function Xorshift128_NextFloat()
	return 1.0 - Xorshift128_NextFloatRange(0.0, 1.0)
end

function Xorshift128_NextFloatRange(min, max)
	return (min - max) * (bit32.lshift(Xorshift128_Next(), 9) / 0xFFFFFFFF) + max;
end

-- Binary logic operations

-- Signed 64-bit to signed 32-bit
-- Replicate C# unchecked "long" to "int" casting, converting the signed 64-bit value (default in Lua) to a signed 32-bit integer by means of overflow
-- This effectively removes the upper 32 bits, but preserves the sign
function overflow32(long)
 
    -- Using integer arithmetic
    --return (long + 2^31) % 2^32 - 2^31
 
    -- Using the bit32 library
    if (bit32.band(long, 0x80000000) ~= 0) then
        return (bit32.band(bit32.bnot(long), 0xFFFFFFFF) + 1) * -1
    else
        return bit32.band(0xFFFFFFFF, long)
    end
end

-- Same as above, but does not preserve sign
function overflow32U(long)
    return bit32.band(0xFFFFFFFF, long)
end

-- This performs multiplication on Lua's double-precision floating point values
-- but generates a result as if the inputs were instead unsigned 32-bit values
-- This is required to emulate uint32 overflow correctly. otherwise, higher
-- order bits are simply truncated and discarded
function uintMultiply(a, b)
	a = a % 4294967296 -- Wrap to length of uint
	b = b % 4294967296 -- for both sides
	local ah, al = math.floor(a / 65536), a % 65536
	local bh, bl = math.floor(b / 65536), b % 65536
	local high = ((ah * bl) + (al * bh)) % 65536
	return ((high * 65536) + (al * bl)) % 4294967296
end

-- Math operations

-- Rotates a vector by a rotation, returning the rotated vector
function quaternionMultVector(qx, qy, qz, qw, vx, vy, vz)
	
	local x = qx * 2;
	local y = qy * 2;
	local z = qz * 2;
	local xx = qx * x;
	local yy = qy * y;
	local zz = qz * z;
	local xy = qx * y;
	local xz = qx * z;
	local yz = qy * z;
	local wx = qw * x;
	local wy = qw * y;
	local wz = qw * z;
	
	-- Return the rotated vector
	rx = (1 - (yy + zz)) * vx + (xy - wz) * vy + (xz + wy) * vz;
	ry = (xy + wz) * vx + (1 - (xx + zz)) * vy + (yz - wx) * vz;
	rz = (xz - wy) * vx + (yz + wx) * vy + (1 - (xx + yy)) * vz;
	return rx, ry, rz;
end

-- Invoke entry point, typically you should instead use this with:
-- {{subst:#invoke:Lootlist|scale|...}}
function p.scale(number, fromMin, fromMax, toMin, toMax)
	return scale(number, fromMin, fromMax, toMin, toMax)
end

-- Scales a float from one range to another
-- For example, scaling a float value 0.5 in the range 0-1 to 0-100 will return 50
function scale(number, fromMin, fromMax, toMin, toMax)
	return (toMax - toMin) * (number - fromMin) / (fromMax - fromMin) + toMin
end

-- Logical and misc operations

function log(str)
	if (LOGGING_ENABLED == true) then mw.log(str) end
end

-- Ordered table iterator, allow to iterate on the natural order of the keys of a
-- table.

function __genOrderedIndex( t )
    local orderedIndex = {}
    for key in pairs(t) do
        table.insert( orderedIndex, key )
    end
    table.sort( orderedIndex )
    return orderedIndex
end

function orderedNext(t, state)
    -- Equivalent of the next function, but returns the keys in the alphabetic
    -- order. We use a temporary ordered key table that is stored in the
    -- table being iterated.

    local key = nil
    --print("orderedNext: state = "..tostring(state) )
    if state == nil then
        -- the first time, generate the index
        t.__orderedIndex = __genOrderedIndex( t )
        key = t.__orderedIndex[1]
    else
        -- fetch the next value
        for i = 1,table.getn(t.__orderedIndex) do
            if t.__orderedIndex[i] == state then
                key = t.__orderedIndex[i+1]
            end
        end
    end

    if key then
        return key, t[key]
    end

    -- no more value to return, cleanup
    t.__orderedIndex = nil
    return
end

function orderedPairs(t)
    -- Equivalent of the pairs() function on tables. Allows to iterate
    -- in order
    return orderedNext, t, nil
end

return p