Module:Items/SourceTables/Sandbox

From Melvor Idle
< Module:Items‎ | SourceTables
Revision as of 08:36, 9 December 2024 by Slash (talk | contribs) (Added 1/x values to Chances; Use Recipe icon for source icons when available; Merge Shop and Upgrade sources into Creation Table; Changed 'Inputs' to 'Costs'; Add output quantity for upgraded items; Fix Ash requirements, apparently)

Documentation for this module may be created at Module:Items/SourceTables/Sandbox/doc

local p = {}

local Constants = require('Module:Constants')
local Shared = require('Module:Shared')
local Common = require('Module:Common')
local GameData = require('Module:GameData')
local SkillData = GameData.skillData
local Modifiers = require('Module:Modifiers')
local Magic = require('Module:Magic')
local Icons = require('Module:Icons')
local Items = require('Module:Items')
local Shop = require('Module:Sandbox/Shop') -- TODO: Remove Sandbox/ when pushing live
local Monsters = require('Module:Monsters')
local Skills = require('Module:Skills')
local Num = require('Module:Number')

local SourceOverrides = {
	['melvorAoD:EarthGolem'] = 'Earth Golem (AoD)'
}

--[[
TODO:
	-- Move Township Task and Cartography Disovery rewards under Item Sources (?)
	-- Center Quantity & Chance in Loot Tables
Creation-Tables:
	-- Move anything that isn't Combat, Thieving, or Chests out of Loot Sources and into Creation Tables (Alt Magic Spells & Gems) (?)
Use-Tables:
	-- Shrink icon size down (to 25px?)
		-- Or change "Item Created" to a single cell? (1 (IMG) Barrier Touch Potion I)
	-- Change Type header to Source
	-- Change Requirements header to Requires
	-- Change XP header to Exp
	-- Change XP display to: Icon - Value - (' XP' or ' AXP' if abyssal)
	-- Remove right alignment on XP, Requirements
	-- Add 'table-na' to cells with N/A
--]]

local function doesRecipeHaveItemID(recipe, itemID)
	if recipe.productId == itemID then
		return true
	elseif Shared.contains(recipe.primaryProducts, itemID) or Shared.contains(recipe.secondaryProducts, itemID) then
		return true
	elseif type(recipe.products) == 'table' then
		return GameData.getEntityByProperty(recipe.products, 'itemID', itemID) ~= nil
	end
	return false
end

function p._getCreationTableData(item, tableData)
	if tableData == nil then tableData = {} end

	local skill = ''
	local reqs = nil
	local source = nil
	local time = 0
	local maxTime = nil
	local lvl = 0
	local isAbyssal = false
	local xp = 0
	local qty = 1
	local costs = nil
	local weight = nil
	local totalWeight = nil

	local itemID = item.id
	--First figure out what skill is used to make this...

	local skillIDs = {
		['Gathering'] = {
			['Farming'] = { recipeKey = 'recipes', recipeType = 'item' },
			['Woodcutting'] = { recipeKey = 'trees', recipeType = 'tree' },
			['Fishing'] = { recipeKey = 'fish', recipeType = 'item' },
			['Firemaking'] = { recipeKey = 'logs', recipeType = 'item' },
			['Mining'] = { recipeKey = 'rockData', recipeType = 'rock' },
			['Cartography'] = { recipeKey = 'paperRecipes', recipeType = 'item' },
			['Harvesting'] = { recipeKey = 'veinData', recipeType = 'vein' }
		},
		['Artisan'] = {
			['Cooking'] = { },
			['Smithing'] = { },
			['Fletching'] = { },
			['Crafting'] = { },
			['Runecrafting'] = { },
			['Herblore'] = { },
			['Summoning'] = { }
		}
	}

	-- Gathering skills
	-- All follow a similar data structure
	for localSkillID, dataProp in pairs(skillIDs.Gathering) do
		local skillData = SkillData[localSkillID]
		local skill = skillData.name
		local reqs, lvl, isAbyssal, xp, qty, source, costs, time, maxTime, weight, totalWeight = nil, 0, false, 0, 1, nil, nil, 0, nil, nil, nil
		for i, recipe in ipairs(skillData[dataProp.recipeKey]) do
			local hasProduct = doesRecipeHaveItemID(recipe, itemID)
			if hasProduct then
				lvl, isAbyssal = Skills.getRecipeLevelRealm(localSkillID, recipe)
				xp = recipe.baseAbyssalExperience or recipe.baseExperience
				qty = recipe.baseQuantity or 1
				reqs = Icons._SkillReq(skill, lvl, false, (isAbyssal and 'melvorItA:Abyssal' or nil))
				-- Source Icon
				if recipe.name ~= nil then
					source = Icons.Icon({ skill, img=recipe.name, type=dataProp.recipeType })
				else
					source = Icons.Icon({ skill, type='skill', class=(isAbyssal and 'abyss-icon' or nil) })
				end
				-- Action time
				if recipe.baseMinInterval ~= nil then
					time = recipe.baseMinInterval / 1000
					if recipe.baseMaxInterval ~= nil then
						maxTime = recipe.baseMaxInterval / 1000
					end
				elseif recipe.baseInterval ~= nil then
					time = recipe.baseInterval / 1000
				elseif skillData.baseInterval ~= nil then
					time = skillData.baseInterval / 1000
				end
				-- Custom Chance, Qty, and Costs data
				if localSkillID == 'Farming' then
					costs = { recipe.seedCost }
					local catData = GameData.getEntityByID(skillData.categories, recipe.categoryID)
					qty = 5 * catData.harvestMultiplier
				elseif localSkillID == 'Firemaking' then
					local itemChanceData = GameData.getEntityByProperty(SkillData.Firemaking.primaryProducts, 'itemID', itemID)
					if itemChanceData ~= nil then
						weight = itemChanceData.chance
					elseif itemID == 'melvorD:Generous_Fire_Spirit' then
						weight = 0.1
					end

					if Shared.contains({ 'melvorD:Generous_Fire_Spirit', 'melvorD:Coal_Ore', 'melvorTotH:Charcoal' }, itemID) then
						costs = 'Any ' .. Icons.Icon({ 'Firemaking', 'Melvor Logs', img='Melvor Logo', section='Melvor Logs' })
					else
						local costItem = Items.getItemByID(recipe.logID)
						costs = Icons.Icon({ costItem.name, type='item', qty=1 })
					end

					if itemID == 'melvorF:Ash' then
						qty = time
					elseif itemID == 'melvorItA:Withered_Ash' or itemID == 'melvorItA:Eternal_Ash' then
						qty = math.max(math.floor(recipe.abyssalLevel / 10), 1)
					end
				elseif localSkillID == 'Cartography' then
					time = 5

					local costItem = Items.getItemByID(recipe.costs.items[1].id)
					costs = Icons.Icon({ costItem.name, type='item', qty=1 })
				elseif localSkillID == 'Harvesting' then
					local itemChanceData = nil
					totalWeight = 0

					for i, product in ipairs(recipe.products) do
						totalWeight = totalWeight + (product.weight or 0)

						if product.itemID == itemID then itemChanceData = product end
					end

					if itemChanceData ~= nil then
						weight = itemChanceData.weight
						reqs = reqs .. '<br>' .. itemChanceData.minIntensityPercent .. '% ' .. Icons.Icon({ recipe.name, type='vein', notext=true }) .. ' Intensity'
					end
				end
				-- Special requirements
				if recipe.totalMasteryRequired ~= nil then
					reqs = reqs .. '<br>' .. Icons.Icon({ 'Mastery', notext=true }) .. ' ' .. Num.formatnum(recipe.totalMasteryRequired) .. ' total [[' .. skill .. ']] [[Mastery]]'
				end
				table.insert(tableData, {
					['skill'] = skill,
					['reqs'] = reqs,
					['isAbyssal'] = isAbyssal,
					['xp'] = xp,
					['costs'] = costs,
					['qty'] = qty,
					['source'] = source,
					['time'] = time,
					['maxTime'] = maxTime,
					['weight'] = weight,
					['totalWeight'] = totalWeight
				})
				-- Most recipes have a single item source or the item source data
				-- is nearly all the same. The following items have some uniqueness
				if not Shared.contains({ 'melvorF:Ash', 'melvorItA:Withered_Ash', 'melvorAoD:Paper' }, itemID) then break end
			end
		end
	end

	-- Artisan skills
	-- Allow follow a similar data structure
	for localSkillID, dataProp in pairs(skillIDs.Artisan) do
		local skillData = SkillData[localSkillID]
		local skill = skillData.name
		local reqs, lvl, isAbyssal, xp, qty, source, costs, time, maxTime = nil, 0, false, 0, 1, Icons.Icon({ skill, type='skill' }), nil, 0, nil
		for i, recipe in ipairs(skillData.recipes) do
			if recipe.productID == itemID or
				(localSkillID == 'Cooking' and recipe.perfectCookID == itemID) or
				(localSkillID == 'Herblore' and Shared.contains(recipe.potionIDs, itemID)) then
				lvl, isAbyssal = Skills.getRecipeLevelRealm(localSkillID, recipe)
				xp = recipe.baseAbyssalExperience or recipe.baseExperience
				qty = recipe.baseQuantity or 1
				reqs = Icons._SkillReq(skill, lvl, false, (isAbyssal and 'melvorItA:Abyssal' or nil))
				source = Icons.Icon({ skill, type='skill', class=(isAbyssal and 'abyss-icon' or nil) })
				-- Action time
				if recipe.baseMinInterval ~= nil then
					time = recipe.baseMinInterval / 1000
					if recipe.baseMaxInterval ~= nil then
						maxTime = recipe.baseMaxInterval / 1000
					end
				elseif recipe.baseInterval ~= nil then
					time = recipe.baseInterval / 1000
				elseif skillData.baseInterval ~= nil then
					time = skillData.baseInterval / 1000
				end
				-- Special requirements
				-- Potions have a mastery level requirement depending on the tier
				if item.charges ~= nil and item.tier ~= nil then
					local levelUnlock = GameData.getEntityByProperty(skillData.masteryLevelUnlocks, 'descriptionID', item.tier + 1)
					if levelUnlock ~= nil then
						reqs = reqs .. '<br>' .. Icons._MasteryReq(item.name, levelUnlock.level, false)
					end
				end
				if localSkillID == 'Cooking' then
					-- Cooking includes the required utility (fire, furnace, pot) as a special requirement
					local cookingCatIcon = {
						["melvorD:Fire"] = 'Normal Cooking Fire',
						["melvorD:Furnace"] = 'Basic Furnace',
						["melvorD:Pot"] = 'Basic Pot'
					}
					local categoryIconName, categoryName = cookingCatIcon[recipe.categoryID], nil
					local recipeCategory = GameData.getEntityByID(SkillData.Cooking.categories, recipe.categoryID)
					if recipeCategory ~= nil then
						categoryName = recipeCategory.modifierName or recipeCategory.name
					end
					if categoryIconName ~= nil and categoryName ~= nil then
						reqs = reqs .. '<br>' .. Icons.Icon({ 'Cooking', categoryName, section = 'Cooking Upgrades', img = categoryIconName, type = 'upgrade' })
					end
				end
				-- Materials & output quantity
				-- Special case for Summoning recipes
				if localSkillID == 'Summoning' then
					local shardCostArray, otherCostArray = {}, {}
					local recipeCost = 0
					if isAbyssal == true then
						recipeCost = skillData.recipeAPCost
					else
						recipeCost = skillData.recipeGPCost
					end
					-- Shards
					for j, itemCost in ipairs(recipe.itemCosts) do
						local shard = Items.getItemByID(itemCost.id)
						if shard ~= nil then
							table.insert(shardCostArray, Icons.Icon({ shard.name, type='item', qty=itemCost.quantity }))
						end
					end
					-- Other costs
					table.insert(otherCostArray, Common.getCostString({ ["items"] = {}, ["currencies"] = recipe.currencyCosts }))
					for j, nonShardID in ipairs(recipe.nonShardItemCosts) do
						local nonShard = Items.getItemByID(nonShardID)
						if nonShard ~= nil then
							local itemValue = math.max(nonShard.sellsFor, 20)
							local nonShardQty = math.max(1, math.ceil(recipeCost / itemValue))
							table.insert(otherCostArray, Icons.Icon({ nonShard.name, type='item', qty=nonShardQty }))
						end
					end
					costs = table.concat(shardCostArray, '<br>')
					if not Shared.tableIsEmpty(otherCostArray) then
						local costLen = Shared.tableCount(otherCostArray)
						costs = costs .. '<br>' .. (costLen == 1 and '' or 'and one of the following:<br>') .. table.concat(otherCostArray, "<br>'''OR''' ")
					end
					reqs = reqs .. '<br>At least 1 ' .. Icons.Icon({ 'Summoning', item.name, img=item.name, type='mark', section='Summoning Marks' }) .. ' mark discovered'
					table.insert(tableData, {
						['skill'] = skill,
						['reqs'] = Icons._SkillReq(skill, lvl, false, (isAbyssal and 'melvorItA:Abyssal' or nil)),
						['isAbyssal'] = isAbyssal,
						['xp'] = xp,
						['costs'] = costs,
						['qty'] = qty,
						['source'] = source,
						['time'] = time
					})
				-- Some items (such as Arrow shafts) have multiple recipes
				elseif type(recipe.alternativeCosts) == 'table' then
					local reqPart, qtyPart = {}, {}
					for j, altCost in ipairs(recipe.alternativeCosts) do
						local costsStr = ''
						for k, itemCost in ipairs(altCost.itemCosts) do
							if k > 1 then costsStr = costsStr .. '<br>' end
							local reqItem = Items.getItemByID(itemCost.id)
							if reqItem == nil then
								costsStr = costsStr .. itemCost.quantity .. 'x ?????'
							else
								costsStr = costsStr .. Icons.Icon({ reqItem.name, type='item', qty=itemCost.quantity })
							end
						end
						costsStr = costsStr .. Common.getCostString({ ["items"] = {}, ["currencies"] = recipe.currencyCosts }, '')
						table.insert(tableData, {
							['skill'] = skill,
							['reqs'] = Icons._SkillReq(skill, lvl, false, (isAbyssal and 'melvorItA:Abyssal' or nil)),
							['isAbyssal'] = isAbyssal,
							['xp'] = xp,
							['costs'] = costsStr,
							['qty'] = qty * altCost.quantityMultiplier,
							['source'] = Icons.Icon({ skill, type='skill' }),
							['time'] = time,
							['maxTime'] = maxTime
						})
					end
				-- Finally, normal recipes with a single set of item costs
				elseif type(recipe.itemCosts) == 'table' and not Shared.tableIsEmpty(recipe.itemCosts) then
					table.insert(tableData, {
						['skill'] = skill,
						['reqs'] = Icons._SkillReq(skill, lvl, false, (isAbyssal and 'melvorItA:Abyssal' or nil)),
						['isAbyssal'] = isAbyssal,
						['xp'] = xp,
						['costs'] = recipe.itemCosts,
						['qty'] = qty,
						['source'] = Icons.Icon({ skill, type='skill' }),
						['time'] = time,
						['maxTime'] = maxTime,
						['currencyCost'] = recipe.currencyCosts
					})
				end
			end
		end
	end

	-- Alt. Magic, excludes spells which can produce a variety of items, such as Gems and Bars
	-- Bars are handled by getItemSuperheatTable()
	-- Gems are handled by _getItemLootSourceTable()
	for i, altSpell in ipairs(Magic.getSpellsBySpellBook('altMagic')) do
		if altSpell.produces == itemID then
			table.insert(tableData, {
				['skill'] = 'Magic',
				['reqs'] = Icons._SkillReq('Magic', altSpell.level, false),
				['isAbyssal'] = false,
				['xp'] = altSpell.baseExperience,
				['costs'] = altSpell.runesRequired,
				['qty'] = altSpell.productionRatio,
				['source'] = Icons.Icon({ altSpell.name, type='spell' }),
				['time'] = 2,
				['altCosts'] = Magic._getAltSpellCostText(altSpell)
			})
		end
	end

	-- Add in Astrology creation items manually since each constellation has (mostly)
	-- the same creation information so looping through them is not necessary
	local stardustChanceData = GameData.getEntityByProperty(SkillData.Astrology.baseRandomItemChances, 'itemID', itemID)
	if stardustChanceData ~= nil then
		local namespace, localID = Shared.getLocalID(stardustChanceData.itemID)
		local isAbyssal = namespace == 'melvorItA'
		table.insert(tableData, {
			['skill'] = 'Astrology',
			['reqs'] = Icons._SkillReq('Astrology', 1, false, (isAbyssal and 'melvorItA:Abyssal' or nil)),
			['isAbyssal'] = isAbyssal,
			['qty'] = qty,
			['xp'] = (isAbyssal and 1238 or 5), -- Use the XP value for the first constellation
			['source'] = Icons.Icon({ 'Astrology', type='skill', class=(isAbyssal and 'abyss-icon' or nil) }),
			['time'] = 3,
			['weight'] = stardustChanceData.chance
		})
	end

	return tableData
end

function p.buildCreationTable(item, tableData)
	if Shared.tableIsEmpty(tableData) then return '' end

	local showSource = false
	local showRequirements = false
	local showInputs = false
	local showOutputs = false
	local showXP = false
	local showTime = false
	local showChance = false
	local colspan = -1 -- colspan only needs to be set when there are 3+ columns in the table

	for i, data in ipairs(tableData) do
		if not showSource and tableData[1].source ~= tableData[i].source then
			showSource = true
			colspan = colspan + 1
		end
		if not showRequirements and tableData[1].reqs ~= tableData[i].reqs then
			showRequirements = true
			colspan = colspan + 1
		end
		if not showInputs and tableData[1].costs ~= tableData[i].costs then
			showInputs = true
			colspan = colspan + 1
		end
		if not showOutputs and tableData[1].qty ~= tableData[i].qty then
			showOutputs = true
			colspan = colspan + 1
		end
		if not showXP and tableData[1].xp ~= tableData[i].xp then
			showXP = true
			colspan = colspan + 1
		end
		if not showTime and tableData[1].time ~= tableData[i].time then
			showTime = true
			colspan = colspan + 1
		end
		if not showChance and tableData[1].weight ~= tableData[i].weight then
			showChance = true
			colspan = colspan + 2
		end
	end

	colspan = math.max(colspan, 1)

	local function addCostsRow(row, data, span)
		local costsRow = row:tag('td'):attr('colspan', span)
		if type(data.costs) == 'table' then
			for i, mat in ipairs(data.costs) do
				if i > 1 then costsRow:tag('br') end
				local matItem = Items.getItemByID(mat.id)
				if matItem == nil then
					costsRow:wikitext(mat.quantity .. 'x ?????')
				else
					costsRow:wikitext(Icons.Icon({ matItem.name, type='item', qty=mat.quantity }))
				end
			end
			
			if data.currencyCost ~= nil then
				costsRow:wikitext('<br>' .. Common.getCostString({ ["items"] = {}, ["currencies"] = data.currencyCost }, ''))
			end
		else
			costsRow:wikitext(data.costs)
		end

		if type(data.altCosts) == 'string' then
			local costStr = data.altCosts:gsub(', ', '<br>')
			costsRow:wikitext('<br>' .. costStr)
		end
	end

	local resultTable = mw.html.create('table')
	resultTable:addClass('wikitable')
	local tableHeader = resultTable:tag('tr')

	if showSource then tableHeader:tag('th'):wikitext('Source') end
	if showRequirements then tableHeader:tag('th'):wikitext('Requires') end
	if showInputs then tableHeader:tag('th'):wikitext('Inputs') end
	if showOutputs then tableHeader:tag('th'):wikitext('Outputs') end
	if showXP then tableHeader:tag('th'):wikitext('Exp') end
	if showTime then tableHeader:tag('th'):wikitext('Time') end
	if showChance then tableHeader:tag('th'):wikitext('Chance'):attr('colspan', 2) end

	-- Populate table data with any unique entries (Ex: Ash's Inputs, Outputs, Exp, Time)
	for i, data in ipairs(tableData) do
		local recipeRow = resultTable:tag('tr')

		if showSource then recipeRow:tag('td'):wikitext(data.source) end
		if showRequirements then recipeRow:tag('td'):wikitext((data.reqs or 'None')) end

		if showInputs and data.costs ~= nil then
			addCostsRow(recipeRow, data, 1)
		elseif showInputs then
			recipeRow:tag('td'):wikitext('N/A'):addClass('table-na')
		end

		if showOutputs then
			if type(data.qty) == 'string' then
				local outputData = recipeRow:tag('td'):wikitext(data.qty)
				if data.center then outputData:addClass('center') end
			else
				recipeRow:tag('td'):wikitext(Icons.Icon({ item.name, type='item', notext=true, qty=(data.qty or 1) })):addClass('center')
			end
		end
		if showXP and data.skill ~= nil and data.xp ~= nil then
			local iconClass = (data.isAbyssal and 'abyss-icon' or nil)
			local xpText = (data.isAbyssal and ' AXP' or ' XP')
			recipeRow:tag('td')
				:wikitext(Icons.Icon({ data.skill, notext=true, type='skill', class=iconClass }))
				:wikitext(' ' .. Num.formatnum(data.xp) .. xpText)
		end
		if showTime and data.time ~= nil then recipeRow:tag('td'):wikitext(Shared.timeString(data.time, true)):addClass('center') end
		if showChance then
			if data.weight ~= nil then
				recipeRow:tag('td'):wikitext(Num.fraction(data.weight, (data.totalWeight or 100)))
				recipeRow:tag('td'):wikitext(Num.round2(data.weight / (data.totalWeight or 100) * 100, 2) .. '%')
			else
				recipeRow:tag('td'):wikitext('100%'):addClass('center'):attr('colspan', 2)
			end
		end
	end

	-- Add all non-unique data below the table data (Ex: Ash's Source, Requires, Chance)
	if not showSource and tableData[1].source ~= nil then
		resultTable:tag('tr')
			:tag('th'):wikitext('Source'):css('text-align', 'right')
				:tag('td'):attr('colspan', colspan):wikitext(tableData[1].source)
	end
	if not showRequirements and tableData[1].reqs ~= nil then
		local reqRow = resultTable:tag('tr')
			:tag('th'):wikitext('Requires'):css('text-align', 'right')
			:tag('td'):wikitext(tableData[1].reqs):attr('colspan', colspan)
	end
	if not showInputs and tableData[1].costs ~= nil then
		local costRow = resultTable:tag('tr')
			:tag('th'):wikitext('Costs'):css('text-align', 'right')

		addCostsRow(costRow, tableData[1], colspan)
	end
	if not showOutputs and tableData[1].qty ~= nil then
		local outputRow = resultTable:tag('tr')
			:tag('th'):wikitext('Outputs'):css('text-align', 'right')

		if type(tableData[1].qty) == 'string' then
			outputRow:tag('td'):wikitext(tableData[1].qty)
		else
			outputRow:tag('td'):attr('colspan', colspan):wikitext(Icons.Icon({ item.name, type='item', qty=(tableData[1].qty or 1) }))
		end
	end
	if not showXP and tableData[1].xp ~= nil then
		local xpText = (tableData[1].isAbyssal and ' AXP' or ' XP')
		resultTable:tag('tr')
			:tag('th'):wikitext('Base Exp'):css('text-align', 'right')
				:tag('td'):attr('colspan', colspan):wikitext(Num.formatnum(tableData[1].xp) .. xpText)
	end
	if not showTime and tableData[1].time ~= nil then
		resultTable:tag('tr')
		local timeHeader = resultTable:tag('th'):wikitext('Base Time'):css('text-align', 'right')

		local timeData = timeHeader:tag('td'):attr('colspan', colspan)
		timeData:wikitext(Shared.timeString(tableData[1].time, true))

		if tableData[1].maxTime ~= nil and tableData[1].maxTime > tableData[1].time then
			timeData:wikitext(' - ' .. Shared.timeString(tableData[1].maxTime, true))
		end
	end
	if not showChance and tableData[1].weight ~= nil then
		local chanceData = resultTable:tag('tr')
			:tag('th'):wikitext('Base Chance'):css('text-align', 'right')
				:tag('td'):attr('colspan', colspan)
					:wikitext(Num.fraction(tableData[1].weight, (tableData[1].totalWeight or 100)))
					:wikitext(' (' .. Num.round2(tableData[1].weight / (tableData[1].totalWeight or 100) * 100, 2) .. '%)')
	end

	return tostring(resultTable)
end

function p.getCreationTable(frame)
	local itemName = frame.args ~= nil and frame.args[1] or frame
	local item = Items.getItem(itemName)
	if item == nil then
		return Shared.printError('No item named "' .. itemName .. '" exists in the data module')
	end

	return p.buildCreationTable(p._getCreationTableData(item), item)
end

function p._getItemSources(item, asList, addCategories, separator)
	local lineArray = {}
	local categoryArray = {}
	local sep = separator or ','

	--Alright, time to go through all the ways you can get an item...
	--First up: Can we kill somebody and take theirs?
	local killStrPart = {}
	for i, monster in ipairs(GameData.rawData.monsters) do
		local isDrop = false
		if monster.bones ~= nil and monster.bones.itemID == item.id and Monsters._getMonsterBones(monster) ~= nil then
			-- Item is a bone, and is either a shard from God dungeons or dropped by a non-boss monster with a loot table
			isDrop = true
		elseif monster.barrierPercent ~= nil and 'melvorAoD:Barrier_Dust' == item.id and not Monsters._isDungeonOnlyMonster(monster) then
			-- Item is Barrier Dust and is not a dungeon exclusive monster
			isDrop = true
		elseif monster.lootTable ~= nil then
			-- If the monster has a loot table, check if the item we are looking for is in there
			-- Dungeon exclusive monsters don't count as they are either:
			--   - A monster before the boss, which doesn't drop anything except shards (checked above)
			--   - A boss monster, whose drops are accounted for in data from Areas instead
			for j, loot in ipairs(monster.lootTable) do
				if loot.itemID == item.id and not Monsters._isDungeonOnlyMonster(monster) then
					isDrop = true
					break
				end
			end
		end
		if isDrop then
			-- Item drops when the monster is killed
			local iconName = monster.name
			if SourceOverrides[monster.id] ~= nil then
				iconName = SourceOverrides[monster.id]
			end
			table.insert(killStrPart, Icons.Icon({iconName, type='monster', notext=true}))
		end
	end
	-- Is the item dropped from any dungeon?
	local dungeonStrPart = {}
	local dungeonEntities = {
		['Dungeon'] = GameData.rawData.dungeons,
		['The Abyss'] = GameData.rawData.abyssDepths
	}
	for entity, dungeons in pairs(dungeonEntities) do
		for i, dungeon in ipairs(dungeons) do
			if (dungeon.oneTimeRewardID ~= nil and item.id == dungeon.oneTimeRewardID) or
				(type(dungeon.rewardItemIDs) == 'table' and Shared.contains(dungeon.rewardItemIDs, item.id)) then
				table.insert(dungeonStrPart, Icons.Icon({dungeon.name, type='combatArea', notext=true}))
			elseif dungeon.eventID ~= nil then
				-- Is the item dropped from a combat event (e.g. Impending Darkness event)?
				local event = GameData.getEntityByID('combatEvents', dungeon.eventID)
				if type(event) == 'table' and type(event.itemRewardIDs) == 'table' then
					for eventCycle, itemRewardID in ipairs(event.itemRewardIDs) do
						if item.id == itemRewardID then
							local dungPrefix = (eventCycle == Shared.tableCount(event.itemRewardIDs) and '' or eventCycle .. (eventCycle == 1 and ' cycle' or ' cycles') .. ' of ')
							table.insert(dungeonStrPart, dungPrefix .. Icons.Icon({dungeon.name, type='combatArea', notext=true}))
							break
						end
					end
				end
			end
		end
	end
	
	for i, stronghold in ipairs(GameData.rawData.strongholds) do
		for tier, tierData in pairs(stronghold.tiers) do
			if type(tierData.rewards) == 'table' and type(tierData.rewards.items) == 'table' then
				for i, reward in ipairs(tierData.rewards.items) do
					if reward.id == item.id then
						table.insert(dungeonStrPart, Icons.Icon({stronghold.name, type='combatArea', notext=true}))
					end
				end
			end
		end
	end

	if not Shared.tableIsEmpty(dungeonStrPart) then
		table.insert(lineArray, 'Completing: ' .. table.concat(dungeonStrPart, sep))
	end
	if not Shared.tableIsEmpty(killStrPart) then
		table.insert(lineArray, 'Killing: ' .. table.concat(killStrPart, sep))
	end

	-- Can we find it in an openable item?
	local lootPart = {}
	for i, item2 in ipairs(GameData.rawData.items) do
		if item2.dropTable ~= nil then
			for j, loot in ipairs(item2.dropTable) do
				if loot.itemID == item.id then
					table.insert(lootPart, Icons.Icon({item2.name, type='item', notext=true}))
					break
				end
			end
		end
	end

	if not Shared.tableIsEmpty(lootPart) then
		table.insert(lineArray, 'Opening: ' .. table.concat(lootPart, sep))
	end

	-- Is the item a result of upgrading/downgrading another item?
	local upgradePart = { up = {}, down = {} }
	for i, upgrade in ipairs(GameData.rawData.itemUpgrades) do
		if item.id == upgrade.upgradedItemID then
			local key = (upgrade.isDowngrade and 'down' or 'up')
			for j, rootItemID in ipairs(upgrade.rootItemIDs) do
				local rootItem = Items.getItemByID(rootItemID)
				if rootItem ~= nil then
					table.insert(upgradePart[key], Icons.Icon({rootItem.name, type='item', notext=true}))
				end
			end
		end
	end

	local upgradeCat = false
	for catName, parts in pairs(upgradePart) do
		if not Shared.tableIsEmpty(parts) then
			if not upgradeCat then
				table.insert(categoryArray, '[[Category:Upgraded Items]]')
				upgradeCat = true
			end
			local typeText = (catName == 'up' and 'Upgrading') or 'Downgrading'
			table.insert(lineArray, typeText .. ': ' .. table.concat(parts, sep))
		end
	end

	--Next: Can we take it from somebody else -without- killing them?
	local thiefItems = Skills.getThievingSourcesForItem(item.id)
	if type(thiefItems) == 'table' then
		local includedNPCs = {}
		local thiefPart = {}
		for i, thiefRow in ipairs(thiefItems) do
			if thiefRow.npc == 'all' then
				--if 'all' is the npc, this is a rare item so just say 'Thieving level 1'
				table.insert(lineArray, Icons._SkillReq('Thieving', 1))
			elseif not Shared.contains(includedNPCs, thiefRow.npc) then
				table.insert(thiefPart, Icons.Icon({thiefRow.npc, type='thieving', notext=true}))
				table.insert(includedNPCs, thiefRow.npc)
			end
		end
		if not Shared.tableIsEmpty(thiefPart) then
			table.insert(lineArray, 'Pickpocketing: ' .. table.concat(thiefPart, sep))
		end
	end
	
	-- Can we get this item by casting an Alt. Magic spell?
	local castPart = {}
	for i, spell in ipairs(Magic.getSpellsProducingItem(item.id)) do
		table.insert(castPart, Icons.Icon({spell.name, type=Magic._getSpellIconType(spell), notext=true}))
	end
	if not Shared.tableIsEmpty(castPart) then
		table.insert(lineArray, 'Casting: ' .. table.concat(castPart, sep))
	end

	--Check if we can make it ourselves
	local skillIDs = {
		['Gathering'] = {
			['Woodcutting'] = { recipeKey = 'trees' },
			['Fishing'] = { recipeKey = 'fish' },
			['Firemaking'] = { recipeKey = 'logs' },
			['Mining'] = { recipeKey = 'rockData' },
			['Farming'] = { recipeKey = 'recipes' },
			['Harvesting'] = { recipeKey = 'veinData' }
		},
		['Artisan'] = {
			['Cooking'] = { },
			['Smithing'] = { },
			['Fletching'] = { },
			['Crafting'] = { },
			['Runecrafting'] = { },
			['Herblore'] = { },
			['Summoning'] = { }
		}
	}

	-- Gathering skills
	for localSkillID, dataProp in pairs(skillIDs.Gathering) do
		local skillData = SkillData[localSkillID]
		local skill = skillData.name
		for i, recipe in ipairs(skillData[dataProp.recipeKey]) do
			local hasProduct = doesRecipeHaveItemID(recipe, item.id)
			if hasProduct then
				if localSkillID == 'Farming' and recipe.seedCost ~= nil then
					local seedItem = Items.getItemByID(recipe.seedCost.id)
					if seedItem ~= nil then
						table.insert(lineArray, 'Growing: ' .. Icons.Icon({seedItem.name, type='item', notext='true'}))
					end
				else
					local level, isAbyssal = Skills.getRecipeLevelRealm(localSkillID, recipe)
					table.insert(lineArray, Icons._SkillReq(skill, level, false, (isAbyssal and "melvorItA:Abyssal" or nil)))
				end
				break
			end
		end
	end

	-- Artisan skills
	for localSkillID, dataProp in pairs(skillIDs.Artisan) do
		local skillData = SkillData[localSkillID]
		local skill = skillData.name
		for i, recipe in ipairs(skillData.recipes) do
			if recipe.productID == item.id or
				(localSkillID == 'Cooking' and recipe.perfectCookID == item.id) or
				(localSkillID == 'Herblore' and Shared.contains(recipe.potionIDs, item.id)) then
				local level, isAbyssal = Skills.getRecipeLevelRealm(localSkillID, recipe)
				table.insert(lineArray, Icons._SkillReq(skill, level, false, (isAbyssal and "melvorItA:Abyssal" or nil)))
				break
			end
		end
	end

	-- Township trading
	for i, tsResource in ipairs(SkillData.Township.itemConversions.fromTownship) do
		local found = false
		for j, tradeDef in ipairs(tsResource.items) do
			if tradeDef.itemID == item.id then
				found = true
				local levelReq = nil
				if tradeDef.unlockRequirements ~= nil then
					for k, req in ipairs(tradeDef.unlockRequirements) do
						if req.type == 'SkillLevel' and req.skillID == 'melvorD:Township' then
							levelReq = req.level
							break
						end
					end
					if levelReq == nil then
						table.insert(lineArray, Icons.Icon({SkillData.Township.name, type='skill'}))
					else
						table.insert(lineArray, Icons._SkillReq(SkillData.Township.name, levelReq))
					end
				end
			end
			if found then
				break
			end
		end
		if found then
			break
		end
	end

	-- Archaeology sources
	-- Digsites
	for i, digsite in ipairs(SkillData.Archaeology.digSites) do
		local found = false
		for artefactType, artefactItems in pairs(digsite.artefacts) do
			for j, itemDef in ipairs(artefactItems) do
				if itemDef.itemID == item.id then
					table.insert(lineArray, Icons._SkillReq(SkillData.Archaeology.name, digsite.level))
					found = true
					break
				end
			end
			if found then
				break
			end
		end
		if found then
			break
		end
	end
	-- Museum rewards
	for i, museumReward in ipairs(SkillData.Archaeology.museumRewards) do
		if type(museumReward.items) == 'table' and Shared.contains(museumReward.items, item.id) then
			table.insert(lineArray, Icons.Icon('Museum'))
			break
		end
	end

	-- Cartography
	-- Paper
	for i, recipe in ipairs(SkillData.Cartography.paperRecipes) do
		if recipe.productId == item.id then
			table.insert(lineArray, Icons.Icon({SkillData.Cartography.name, type='skill'}))
			break
		end
	end
	-- POI discovery rewards
	for i, worldMap in ipairs(SkillData.Cartography.worldMaps) do
		local found = false
		for j, poi in ipairs(worldMap.pointsOfInterest) do
			if type(poi.discoveryRewards) == 'table' and type(poi.discoveryRewards.items) == 'table' then
				for k, itemDef in ipairs(poi.discoveryRewards.items) do
					if itemDef.id == item.id then
						-- Find level for POI hex
						local level = 1
						local poiHex = nil
						local skillID = SkillData.Cartography.skillID
						for m, hex in ipairs(worldMap.hexes) do
							if hex.coordinates.q == poi.coords.q and hex.coordinates.r == poi.coords.r then
								for n, req in ipairs(hex.requirements) do
									if req.type == 'SkillLevel' and req.skillID == skillID then
										level = req.level
										break
									end
								end
								break
							end
						end
						table.insert(lineArray, Icons._SkillReq(SkillData.Cartography.name, level))
						found = true
						break
					end
				end
				if found then
					break
				end
			end
		end
		if found then
			break
		end
	end
	-- Travel events
	for i, event in ipairs(SkillData.Cartography.travelEvents) do
		local found = false
		if type(event.rewards) == 'table' and type(event.rewards.items) == 'table' then
			for j, itemDef in ipairs(event.rewards.items) do
				if itemDef.id == item.id and itemDef.quantity > 0 then
					table.insert(lineArray, Icons.Icon({SkillData.Cartography.name, type='skill'}))
					found = true
					break
				end
			end
			if found then
				break
			end
		end
	end

	--AstrologyCheck
	for i, dustDrop in ipairs(SkillData.Astrology.baseRandomItemChances) do
		if dustDrop.itemID == item.id then
			table.insert(lineArray, Icons.Icon({SkillData.Astrology.name, type='skill'}))
		end
	end

	-- Woodcutting
	-- Raven Nest
	if item.id == SkillData.Woodcutting.ravenNestItemID then
		local levelReq = nil
		for i, tree in ipairs(SkillData.Woodcutting.trees) do
			if tree.canDropRavenNest and (levelReq == nil or tree.level < levelReq) then
				levelReq = tree.level
			end
		end
		table.insert(lineArray, Icons._SkillReq(SkillData.Woodcutting.name, levelReq))
	-- Bird Nest, Ash, and Mushroom
	elseif Shared.contains({
		SkillData.Woodcutting.nestItemID,
		SkillData.Woodcutting.ashItemID,
		SkillData.Woodcutting.mushroomItemID
		}, item.id) then
		table.insert(lineArray, Icons._SkillReq(SkillData.Woodcutting.name, 1))
	end

	-- Fishing
	-- Junk
	if Shared.contains(SkillData.Fishing.junkItemIDs, item.id) then
		table.insert(lineArray, Icons.Icon({'Fishing', type='skill', notext=true}) .. ' [[Fishing#Junk|Junk]]')
	elseif item.id == SkillData.Fishing.lostChestItem then
		table.insert(lineArray, Icons._SkillReq(SkillData.Fishing.name, 100))
	end
	-- Specials
	for i, specialItem in ipairs(SkillData.Fishing.specialItems) do
		if GameData.getEntityByProperty(specialItem.drops, 'itemID', item.id) ~= nil then
			table.insert(lineArray, Icons.Icon({'Fishing', type='skill', notext=true}) .. ' [[Fishing#Special|Special]]')
		end
	end

	-- Firemaking: Coal
	if Shared.contains({SkillData.Firemaking.coalItemID,
		SkillData.Firemaking.ashItemID,
		SkillData.Firemaking.charcoalItemID,
		SkillData.Firemaking.fireSpiritItemID,
		SkillData.Firemaking.diamondItemID
		}, item.id) then
		table.insert(lineArray, Icons._SkillReq(SkillData.Firemaking.name, 1))
	end

	-- Mining: Gems
	if (GameData.getEntityByProperty('randomGems', 'itemID', item.id) ~= nil or
		GameData.getEntityByProperty('randomSuperiorGems', 'itemID', item.id) ~= nil or
		GameData.getEntityByProperty('randomAbyssalGems', 'itemID', item.id) ~= nil) then
		table.insert(lineArray, Icons.Icon({"Mining", type='skill', notext=true})..' [[Mining#Gems|Gem]]')
	elseif item.id == SkillData.Mining.runestoneItemID then
		-- From pure essence mining
		local recipe = GameData.getEntityByID(SkillData.Mining.rockData, 'melvorTotH:Pure_Essence')
		if recipe ~= nil then
			table.insert(lineArray, Icons._SkillReq(SkillData.Mining.name, recipe.level))
		end
	end

	-- General rare drops for non-combat skills
	-- Includes items like Circlet/Jewel of Rhaelyx, Mysterious stones, Signet ring half (a),
	-- relics (for Ancient Relics mode)
	local skillIconList, subText = {}, ''
	for i, skillDataAll in ipairs(GameData.rawData.skillData) do
		local skillData = skillDataAll.data
		local skillName, displaySkillName = skillData.name, false
		-- All general rare drops within the Magic are for Alt. Magic
		if skillDataAll.skillID == 'melvorD:Magic' then
			skillName, displaySkillName = 'Alt. Magic', true
		end
		if type(skillData.rareDrops) == 'table' then
			for j, rareDrop in ipairs(skillData.rareDrops) do
				local isAltItem = (rareDrop.altItemID ~= nil and rareDrop.altItemID == item.id)
				if isAltItem or rareDrop.itemID == item.id then
					if Shared.tableIsEmpty(skillIconList) then
						-- Initialize subText
						if isAltItem then
							local wornItem = Items.getItemByID(rareDrop.itemID)
							subText = ' while wearing ' .. Icons.Icon({wornItem.name, type='item'})
						elseif rareDrop.altItemID ~= nil then
							-- There exists an alt item, but we are not searching for it
							local altItem = Items.getItemByID(rareDrop.altItemID)
							subText = ' if not worn (Instead of ' .. Icons.Icon({altItem.name, type='item'}) .. ')'
						elseif rareDrop.itemID == 'melvorD:Mysterious_Stone' then
							local foundItem = Items.getItemByID('melvorD:Crown_of_Rhaelyx')
							subText = '<br>after finding ' .. Icons.Icon({foundItem.name, type='item'})
						end
						if type(rareDrop.gamemodes) == 'table' then
							local gamemodeText = {}
							for k, gamemodeID in ipairs(rareDrop.gamemodes) do
								local gamemode = GameData.getEntityByID('gamemodes', gamemodeID)
								if gamemode ~= nil then
									table.insert(gamemodeText, gamemode.name)
								end
							end
							if not Shared.tableIsEmpty(gamemodeText) then
								subText = subText .. ' (' .. table.concat(gamemodeText, ', ') .. ' only)'
							end
						end
					end
					local skillText = Icons.Icon({skillName, type='skill', notext=true})
					if displaySkillName then
						skillText = skillText .. ' (' .. Icons.Icon({skillName, type='skill', noicon=true}) .. ')'
					end
					table.insert(skillIconList, skillText)
				end
			end
		end
	end
	if not Shared.tableIsEmpty(skillIconList) then
		table.insert(lineArray, 'Any action in: ' .. table.concat(skillIconList, ', ') .. subText)
		skillIconList, subText = {}, ''
	end

	-- Supplementary stuff on top of general rare drops
	if item.id == 'melvorD:Gold_Topaz_Ring' then
		table.insert(lineArray, 'Killing any monster if not worn (Instead of '..Icons.Icon({"Signet Ring Half (b)", type="item"})..')')
	elseif item.id == 'melvorD:Signet_Ring_Half_B' then
		table.insert(lineArray, 'Killing any monster while wearing '..Icons.Icon({'Gold Topaz Ring', type='item'}))
	elseif item.id == 'melvorTotH:Deadly_Toxins_Potion' then
		--Adding a special override for Deadly Toxins potions
		table.insert(lineArray, 'Brewing [[Lethal Toxins Potion]]s while wearing '..Icons.Icon({'Toxic Maker Gloves', type='item'}))
	end

	--Tokens are from the appropriate skill
	if item.modifiers ~= nil and item.modifiers.masteryToken ~= nil then
		for localSkillID, skillData in pairs(SkillData) do
			if skillData.masteryTokenID ~= nil and skillData.masteryTokenID == item.id then
				table.insert(lineArray, Icons._SkillReq(skillData.name, 1))
				break
			end
		end
	end

	-- Golbin Raid exclusive items
	if item.golbinRaidExclusive then
		table.insert(lineArray, Icons.Icon({'Golbin Raid', type='pet', img='Golden Golbin'}))
	end

	--Shop items (including special items like gloves that aren't otherwise listed)
	if not Shared.tableIsEmpty(Shop.getItemSourceArray(item.id)) then
		table.insert(lineArray, Icons.Icon({'Shop'}))
	end

	--Easter Eggs (manual list 'cause don't have a better way to do that)
	if Shared.contains(Items.EasterEggs, item.name) then
		table.insert(lineArray, '[[Easter Eggs]]')
	end
	-- Event exclusive items (also a manual list)
	if Shared.contains(Items.EventItems, item.name) then
		table.insert(lineArray, '[[Events]]')
	end

	-- Township Task reward
	for _, task in ipairs(SkillData.Township.tasks) do
		if task.rewards.items[1] ~= nil then -- Skip tasks with no items
			if GameData.getEntityByID(task.rewards.items, item.id) then
				table.insert(lineArray, Icons.Icon({'Tasks', type='township'}))
				break
			end
		end
	end

	local resultPart = {}
	if asList then
		table.insert(resultPart, '* '..table.concat(lineArray, "\r\n* "))
	else
		table.insert(resultPart, '<div style="max-width:180px;text-align:right">' .. table.concat(lineArray, "<br>") .. '</div>')
	end
	if addCategories then table.insert(resultPart, table.concat(categoryArray, '')) end
	return table.concat(resultPart)
end

function p.getItemSources(frame)
	local itemName = frame.args ~= nil and frame.args[1] or frame
	local item = Items.getItem(itemName)
	local asList = false
	local addCategories = false
	if frame.args ~= nil then
		asList = frame.args.asList ~= nil and frame.args.asList ~= '' and frame.args.asList ~= 'false'
		addCategories = frame.args.addCategories ~= nil and frame.args.addCategories ~= '' and frame.args.addCategories ~= 'false'
	end
	if item == nil then
		return Shared.printError('No item named "' .. itemName .. '" exists in the data module')
	end

	return p._getItemSources(item, asList, addCategories)
end

function p._getItemLootSourceTable(item)
	local resultPart = {}
	table.insert(resultPart, '{| class="wikitable sortable stickyHeader"')
	table.insert(resultPart, '\r\n|- class="headerRow-0"')
	table.insert(resultPart, '\r\n!Source!!Level!!Quantity!!colspan="2"|Chance')

	--Set up function for adding rows
	local buildRow = function(source, level, levelNum, minqty, qty, weight, totalWeight, expIcon)
		if minqty == nil then minqty = 1 end
		if expIcon == nil then expIcon = '' end
		if level == nil then level = 'N/A' end
		local rowPart = {}
		table.insert(rowPart, '\r\n|-')
		table.insert(rowPart, '\r\n|style="text-align: left;"|'..source)
		-- Retrieve numeric level value for sorting, or remove anything between [[]]
		local levelValue = ''
		if levelNum ~= nil then
			levelValue = tostring(levelNum)
		else
			levelValue = level:match('%[%[.-%]%]%s*(%w+)$') or ''
		end
		table.insert(rowPart, '\r\n|style="text-align: left;" data-sort-value="'..levelValue..'"|'..expIcon..' '..level)
		table.insert(rowPart, '\r\n|style="text-align: right;" data-sort-value="'..qty..'"|'..Num.formatnum(minqty))
		if qty ~= minqty then table.insert(rowPart, ' - '..Num.formatnum(qty)) end
		local chance = weight / totalWeight * 100
		-- If chance is less than 0.10% then show 2 significant figures, otherwise 2 decimal places
		local fmt = (chance < 0.10 and '%.2g') or '%.2f'
		local chanceStr = string.format(fmt, chance)
		if weight >= totalWeight then
			-- Fraction would be 1/1, so only show the percentage
			chanceStr = '100'
			table.insert(rowPart, '\r\n|colspan="2" ')
		else
			local fraction = Num.fraction(weight, totalWeight)
			if Shared.contains(fraction, '%.') then
				--If fraction contains decimals, something screwy happened so just show only percentage
				--(happens sometimes with the rare thieving items)
				table.insert(rowPart, '\r\n|colspan="2" ')
			else
				table.insert(rowPart, '\r\n|style="text-align: right;" data-sort-value="' .. chanceStr .. '"| ' .. Num.fraction(weight, totalWeight) .. '\r\n|')
			end
		end
		if weight == -1 then
			--Weight of -1 means this is a weird row that has a variable percentage
			table.insert(rowPart, 'style="text-align: right;" data-sort-value="0"|Varies (see Thieving page)')
		else
			table.insert(rowPart, 'style="text-align: right;" data-sort-value="'.. chanceStr .. '"|'..chanceStr..'%')
		end
		return table.concat(rowPart)
	end
	local dropRows = {}

	--Alright, time to go through a few ways to get the item
	--First up: Can we kill somebody and take theirs?
	for i, drop in ipairs(p._getItemMonsterSources(item)) do
		local monster = GameData.getEntityByID('monsters', drop.id)
		local iconName = monster.name
		if SourceOverrides[drop.id] ~= nil then
			iconName = SourceOverrides[drop.id]
		end

		if monster ~= nil then
			local monsterLevel = Monsters._getMonsterCombatLevel(monster)
			table.insert(dropRows, {
				source = Icons.Icon({iconName, type='monster'}), 
				level = Icons.Icon({'Combat', 'Monsters', notext=true}) .. ' Level ' .. Num.formatnum(monsterLevel),
				levelNum = monsterLevel,
				minqty = drop.minQty, 
				qty = drop.maxQty, 
				weight = drop.dropWt, 
				totalWeight = drop.totalWt, 
				expIcon = Icons.getExpansionIcon(drop.id)})
		end
	end
	
	--Patching in here because it uses the same format
	--Can we find this in an Archaeology digsite?
	for i, drop in ipairs(p._getItemArchSources(item)) do
		if drop.name ~= nil then
			table.insert(dropRows, {
				source = Icons.Icon({drop.name, type='poi'}), 
				level = Icons._SkillReq('Archaeology', drop.level) .. ' ('..drop.size..')',
				levelNum = drop.level,
				minqty = drop.minQty, 
				qty = drop.maxQty, 
				weight = drop.dropWt, 
				totalWeight = drop.totalWt, 
				expIcon = Icons.getExpansionIcon(drop.id)})
		end
	end

	-- Is the item dropped from any dungeon?
	local dungeonEntities = {
		['Dungeon'] = GameData.rawData.dungeons,
		['The Abyss'] = GameData.rawData.abyssDepths
	}
	for entity, dungeons in pairs(dungeonEntities) do
		for i, dungeon in ipairs(dungeons) do
			if (dungeon.oneTimeRewardID ~= nil and item.id == dungeon.oneTimeRewardID) or
				(type(dungeon.rewardItemIDs) == 'table' and Shared.contains(dungeon.rewardItemIDs, item.id)) then
					table.insert(dropRows, {
						source = Icons.Icon({dungeon.name, type='combatArea'}), 
						level = '[['..entity..']]',
						minqty = 1, 
						qty = 1, 
						weight = 1, 
						totalWeight = 1, 
						expIcon = Icons.getExpansionIcon(dungeon.id)})
			elseif dungeon.eventID ~= nil then
				-- Is the item dropped from a combat event (e.g. Impending Darkness event)?
				local event = GameData.getEntityByID('combatEvents', dungeon.eventID)
				if type(event) == 'table' and type(event.itemRewardIDs) == 'table' then
					for eventCycle, itemRewardID in ipairs(event.itemRewardIDs) do
						if item.id == itemRewardID then
							local sourceTxt = Icons.Icon({dungeon.name, type='combatArea'}) .. (eventCycle == Shared.tableCount(event.itemRewardIDs) and '' or ', Cycle ' .. eventCycle)
							table.insert(dropRows, {
								source = sourceTxt, 
								level = '[['..entity..']]',
								minqty = 1, 
								qty = 1, 
								weight = 1, 
								totalWeight = 1})
							break
						end
					end
				end
			end
		end
	end

	for i, stronghold in ipairs(GameData.rawData.strongholds) do
		for tier, tierData in pairs(stronghold.tiers) do
			if type(tierData.rewards) == 'table' and type(tierData.rewards.items) == 'table' then
				for i, reward in ipairs(tierData.rewards.items) do
					if reward.id == item.id then
						table.insert(dropRows, {
							source = Icons.Icon({stronghold.name, type='combatArea'}), 
							level = '[[Strongholds|'..tier..']]',
							minqty = 1, 
							qty = 1, 
							weight = tierData.rewards.chance, 
							totalWeight = 100, 
							expIcon = Icons.getExpansionIcon(stronghold.id)})
					end
				end
			end
		end
	end

	-- Can we find it in an openable item?
	for i, item2 in ipairs(GameData.rawData.items) do
		if item2.dropTable ~= nil then
			local minQty, maxQty, wt, totalWt = 1, 1, 0, 0
			for j, loot in ipairs(item2.dropTable) do
				totalWt = totalWt + loot.weight
				if loot.itemID == item.id then
					wt = loot.weight
					minQty = loot.minQuantity
					maxQty = loot.maxQuantity
				end
			end

			if wt > 0 then
				local sourceTxt = Icons.Icon({item2.name, type='item'})
				table.insert(dropRows, {
					source = sourceTxt, 
					level = '[[Chest]]',
					minqty = minQty, 
					qty = maxQty, 
					weight = wt, 
					totalWeight = totalWt, 
					expIcon = Icons.getExpansionIcon(item2.id)})
			end
		end
	end

	-- Can it be obtained from Thieving?
	local thiefItems = Skills.getThievingSourcesForItem(item.id)
	for i, thiefRow in ipairs(thiefItems) do
		local sourceTxt = ''
		if thiefRow.npc == 'all' then
			sourceTxt = 'Thieving Rare Drop'
		else
			sourceTxt = Icons.Icon({thiefRow.npc, type='thieving'})
		end
		local levelNum = thiefRow.abyssalLevel or thiefRow.level
		local isAbyssal = thiefRow.abyssalLevel ~= nil
		table.insert(dropRows, {
			source = sourceTxt,
			level = Icons._SkillReq("Thieving", levelNum, false, (isAbyssal and "melvorItA:Abyssal" or nil)),
			levelNum = levelNum,
			minqty = thiefRow.minQty, 
			qty = thiefRow.maxQty, 
			weight = thiefRow.wt, 
			totalWeight = thiefRow.totalWt, 
			expIcon = Icons.getExpansionIcon(thiefRow.npcID)})
	end

	-- Fishing: Junk & Specials
	if Shared.contains(SkillData.Fishing.junkItemIDs, item.id) then
		local fishSource = '[[Fishing#Junk|Junk]]'
		local fishType = Icons.Icon({'Fishing', type='skill'})
		local fishTotWeight = Shared.tableCount(SkillData.Fishing.junkItemIDs)
		table.insert(dropRows, {
			source = fishSource, 
			level = Icons._SkillReq("Fishing", 1),
			levelNum = 1,
			minqty = 1, 
			qty = 1, 
			weight = 1, 
			totalWeight = fishTotWeight})
	else
		local fishTotWeight, fishItem, realmID = {['melvorD:Melvor'] = 0, ['melvorItA:Abyssal'] = 0}, nil, nil
		for i, specialItem in ipairs(SkillData.Fishing.specialItems) do
			for f, drop in ipairs(specialItem.drops) do
				if drop.itemID == item.id then
					fishItem = drop
					realmID = specialItem.realmID
				end
				fishTotWeight[specialItem.realmID] = fishTotWeight[specialItem.realmID] + drop.weight
			end
		end
		if fishItem ~= nil then
			local fishSource = '[[Fishing#Special|Special]]'
			local fishType = Icons.Icon({SkillData.Fishing.name, type='skill'})
			table.insert(dropRows, {
				source = fishSource, 
				level = Icons._SkillReq("Fishing", 1, false, realmID),
				levelNum = 1,
				minqty = fishItem.minQuantity, 
				qty = fishItem.maxQuantity, 
				weight = fishItem.weight, 
				totalWeight = fishTotWeight[realmID]})
		end
	end

	-- Mining: Gems, and also Alt. Magic spells producing random gems
	if Shared.contains({'Gem', 'Superior Gem', 'Abyssal Gem'}, item.type) then
		local gemKeys = { 'randomGems', 'randomSuperiorGems', 'randomAbyssalGems' }
		for i, gemKey in ipairs(gemKeys) do
			local thisGem, totalGemWeight = nil, 0
			for j, gem in ipairs(GameData.rawData[gemKey]) do
				totalGemWeight = totalGemWeight + gem.weight
				if gem.itemID == item.id then
					thisGem = gem
				end
			end
			if thisGem ~= nil then
				local expIcon = ''
				local sourceTxt
				local lv = nil
				if item.type == 'Superior Gem' then
					expIcon = Icons.TotH()
					sourceTxt = '[[Mining#Superior Gems|Superior Gem]]'
					-- Superior gems can only be found with Mining 100 or above
					lv = 100
				else
					sourceTxt = '[[Mining#Gems|Gem]]'
					-- Gems can only be found with any Mining level
					lv = 1
				end
				table.insert(dropRows, {
					source = sourceTxt, 
					level = Icons._SkillReq('Mining', lv),
					levelNum = lv,
					minqty = thisGem.minQuantity, 
					qty = thisGem.maxQuantity, 
					weight = thisGem.weight, 
					totalWeight = totalGemWeight, 
					expIcon = expIcon})
				
				-- Check for Alt. Magic spells also
				local producesKey = (gemKey == 'randomGems' and 'RandomGem') or (gemKey == 'randomSuperiorGems' and 'RandomSuperiorGem') or nil
				if producesKey ~= nil then
					for j, spell in ipairs(Magic.getSpellsBySpellBook('altMagic')) do
						if spell.produces ~= nil and spell.produces == producesKey then
							table.insert(dropRows, {
								source = Icons.Icon({spell.name, type=Magic._getSpellIconType(spell)}), 
								level = Icons.Icon({'Alternative Magic', type='skill', img='Magic', notext=true}) .. ' Level ' .. spell.level,
								levelNum = spell.level,
								minqty = thisGem.minQuantity, 
								qty = thisGem.maxQuantity,
								weight = thisGem.weight, 
								totalWeight = totalGemWeight,
								expIcon = Icons.getExpansionIcon(spell.id)})
						end
					end
				end
			end
		end
	end

	--Make sure to return nothing if there are no drop sources
	if Shared.tableIsEmpty(dropRows) then return '' end
	
	table.sort(dropRows, function(a, b)
							if a.weight / a.totalWeight == b.weight / b.totalWeight then
								if a.minqty + a.qty == b.minqty + b.qty then
									return (a.level == b.level and a.source < b.source) or a.level < b.level
								else
									return a.minqty + a.qty > b.minqty + b.qty
								end
							else
								return a.weight / a.totalWeight > b.weight / b.totalWeight
							end
						end)
	for i, data in ipairs(dropRows) do
		table.insert(resultPart, buildRow(data.source, data.level, data.levelNum, data.minqty, data.qty, data.weight, data.totalWeight, data.expIcon))
	end

	table.insert(resultPart, '\r\n|}')
	return table.concat(resultPart)
end

function p.getItemLootSourceTable(frame)
	local itemName = frame.args ~= nil and frame.args[1] or frame
	local item = Items.getItem(itemName)
	if item == nil then
		return Shared.printError('No item named "' .. itemName .. '" exists in the data module')
	end

	return p._getItemLootSourceTable(item)
end

function p._getItemUpgradeTableData(item, tableData)
	if tableData == nil then tableData = {} end

	local upgrade = GameData.getEntityByProperty('itemUpgrades', 'upgradedItemID', item.id)
	if upgrade ~= nil then
		local upgradeCost = Common.getCostString({
			["items"] = upgrade.itemCosts,
			["currencies"] = upgrade.currencyCosts
		})

		table.insert(tableData, {
			['costs'] = upgradeCost,
			['qty'] = 1,
			['source'] = '[[Upgrading Items|Item Upgrade]]'
		})
	end

	return tableData
end

function p.getItemUpgradeTable(frame)
	local itemName = frame.args ~= nil and frame.args[1] or frame
	local item = Items.getItem(itemName)
	if item == nil then
		return Shared.printError('No item named "' .. itemName .. '" exists in the data module')
	end

	return p.buildCreationTable(p._getItemUpgradeTableData(item), item)
end

function p._getSuperheatSmithRecipe(item)
	local smithRecipe = GameData.getEntityByProperty(SkillData.Smithing.recipes, 'productID', item.id)
	if smithRecipe ~= nil and smithRecipe.categoryID == 'melvorD:Bars' then
		return smithRecipe
	end
end

function p._getItemSuperheatTable(item)
	--Manually build the Superheat Item table
	-- Validate that the item can be superheated
	local smithRecipe = p._getSuperheatSmithRecipe(item)
	if smithRecipe == nil then
		return Shared.printError('The item "' .. item.name .. '" cannot be superheated')
	end

	local oreStringPart, coalString = {}, ''
	for i, mat in ipairs(smithRecipe.itemCosts) do
		local matItem = Items.getItemByID(mat.id)
		if mat.id == 'melvorD:Coal_Ore' then
			coalString = Icons.Icon({matItem.name, type='item', notext='true', qty=mat.quantity})
		else
			table.insert(oreStringPart, Icons.Icon({matItem.name, type='item', notext='true', qty=mat.quantity}))
		end
	end

	--Set up the header
	local superheatTable = {}
	table.insert(superheatTable, '{|class="wikitable"\r\n!colspan="2"|Spell')
	table.insert(superheatTable, '!!'..Icons.Icon({'Smithing', type='skill', notext='true'})..' Level')
	table.insert(superheatTable, '!!'..Icons.Icon({'Magic', type='skill', notext='true'})..' Level')
	table.insert(superheatTable, '!!'..Icons.Icon({'Magic', type='skill', notext='true'})..' XP')
	table.insert(superheatTable, '!!'..Icons.Icon({item.name, type='item', notext='true'})..' Bars')
	table.insert(superheatTable, '!!Ore!!Runes')

	--Loop through all the variants
	local spells = Magic.getSpellsProducingItem(item.id)
	for i, spell in ipairs(spells) do
		if spell.specialCost ~= nil and Shared.contains({ 'BarIngredientsWithCoal', 'BarIngredientsWithoutCoal' }, spell.specialCost.type) then
			local imgType = Magic._getSpellIconType(spell)
			table.insert(superheatTable, '\r\n|-\r\n|'..Icons.Icon({spell.name, type=imgType, notext=true, size=50}))
			table.insert(superheatTable, '||'..Icons.Icon({spell.name, type=imgType, noicon=true})..'||style="text-align:right;"|'..smithRecipe.level)
			table.insert(superheatTable, '||style="text-align:right;"|'..spell.level..'||style="text-align:right;"|'..spell.baseExperience)
			table.insert(superheatTable, '||style="text-align:right;"|'..spell.productionRatio)
			table.insert(superheatTable, '|| '..table.concat(oreStringPart, ', '))
			if spell.specialCost.type == 'BarIngredientsWithCoal' and coalString ~= '' then
				table.insert(superheatTable, (not Shared.tableIsEmpty(oreStringPart) and ', ' or '') .. coalString)
			end
			table.insert(superheatTable, '||style="text-align:center"| ' .. Magic._getSpellRunes(spell))
		end
	end

	--Add the table end and add the table to the result string
	table.insert(superheatTable, '\r\n|}')
	return table.concat(superheatTable)
end

function p.getItemSuperheatTable(frame)
	local itemName = frame.args ~= nil and frame.args[1] or frame
	local item = Items.getItem(itemName)
	if item == nil then
		return Shared.printError('No item named "' .. itemName .. '" exists in the data module')
	end

	return p._getItemSuperheatTable(item)
end

function p._getTownshipTraderTable(item)
	for i, tsResource in ipairs(SkillData.Township.itemConversions.fromTownship) do
		for j, tradeDef in ipairs(tsResource.items) do
			if tradeDef.itemID == item.id then
				-- Item found, build table
				local res = GameData.getEntityByID(SkillData.Township.resources, tsResource.resourceID)
				local resName = (res ~= nil and res.name) or 'Unknown'
				local resQty = math.max(item.sellsFor, 2)

				local resultPart = {}
				table.insert(resultPart, '{| class="wikitable"\n|-')
				table.insert(resultPart, '\n!colspan="2"| ' .. Icons.Icon({'Township', 'Trader', type='skill'}))
				table.insert(resultPart, '\n|-\n!style="text-align:right;"| Cost')
				table.insert(resultPart, '\n| ' .. Icons.Icon({resName, qty=resQty, type='resource'}))
				table.insert(resultPart, '\n|-\n!style="text-align:right;| Requirements')
				table.insert(resultPart, '\n| ' .. Shop.getRequirementString(tradeDef.unlockRequirements))
				table.insert(resultPart, '\n|}')

				return table.concat(resultPart)
			end
		end
	end
	return ''
end
-- TODO: Remove this maybe?
function p._getItemShopTableData(item, tableData)
	if tableData == nil then tableData = {} end

	local purchaseArray = Shop.getItemSourceArray(item.id)

	for i, purchaseData in ipairs(purchaseArray) do
		local purchase = purchaseData.purchase
		local namespace, localID = Shared.getLocalID(purchase.id)
		local source = nil
		-- Show icon text when it's the only entry in the table
		local notext = (Shared.tableCount(tableData) + Shared.tableCount(purchaseArray) > 1)

		if purchase.contains.items ~= nil and Shared.tableCount(purchase.contains.items) > 1 then
			source = Shop._getPurchaseExpansionIcon(purchase) .. Common.getPurchaseIcon({purchase})
			-- Always show icon text when there's multiple items
			notext = false
		else
			source = Icons.Icon({'Shop'}) .. ' Purchase'
		end

		table.insert(tableData, {
			['reqs'] = Common.getRequirementString(purchase.purchaseRequirements, 'None'),
			['isAbyssal'] = namespace == 'melvorItA',
			['costs'] = Shop.getCostString(purchase.cost, false),
			['qty'] = Shop._getPurchaseContents(purchase, true, notext),
			['source'] = source,
			['center'] = notext
		})
	end

	return tableData
end

function p._getItemSourceTables(item)
	local resultPart = {}
	local sourceData = {}

	p._getCreationTableData(item, sourceData)
	p._getItemUpgradeTableData(item, sourceData)
	p._getItemShopTableData(item, sourceData)

	local sourceTable = p.buildCreationTable(item, sourceData)
	if sourceTable ~= '' then
		 table.insert(resultPart, sourceTable)
	end
	--[[ TODO: Uncomment this section

	local townshipTable = p._getTownshipTraderTable(item)
	if townshipTable ~= '' then
		if not Shared.tableIsEmpty(resultPart) then table.insert(resultPart, '\n') end
		table.insert(resultPart, '===Township===\n' .. townshipTable)
	end

	if p._getSuperheatSmithRecipe(item) ~= nil then
		table.insert(resultPart, '\r\n==='..Icons.Icon({'Alt. Magic', type='skill'})..'===\r\n'..p._getItemSuperheatTable(item))
	end

	local lootTable = p._getItemLootSourceTable(item)
	if lootTable ~= '' then
		if not Shared.tableIsEmpty(resultPart) then table.insert(resultPart, '\r\n') end
		table.insert(resultPart, '===Loot===\r\n'..lootTable)
	end
	--]]
	return table.concat(resultPart)
end

function p.getItemSourceTables(frame)
	local itemName = frame.args ~= nil and frame.args[1] or frame
	local item = Items.getItem(itemName)
	if item == nil then
		return Shared.printError('No item named "' .. itemName .. '" exists in the data module')
	end

	return p._getItemSourceTables(item)
end

function p.getCombatPassiveSlotItems(frame)
	local resultPart = {}
	table.insert(resultPart, '{| class="wikitable"\r\n')
	table.insert(resultPart, '|-\r\n')
	table.insert(resultPart, '!colspan="2"|Item\r\n! Passive\r\n')

	local itemArray = Items.getItems(function(item) return item.validSlots ~= nil and (item.golbinRaidExclusive == nil or not item.golbinRaidExclusive) and Shared.contains(item.validSlots, 'Passive') end)

	for i, item in ipairs(itemArray) do
		local passiveDesc = item.customDescription or Modifiers.getModifiersText(item.modifiers, false, false, 10)
		table.insert(resultPart, '|-\r\n')
		table.insert(resultPart, '! '..Icons.Icon({item.name, type='item', notext='true'})..'\r\n! '..Icons.Icon({item.name, type='item', noicon=true})..'\r\n')
		table.insert(resultPart, '| '..passiveDesc..'\r\n')
	end

	table.insert(resultPart, '|}')

	return table.concat(resultPart)
end

function p._getItemMonsterSources(item)
	local resultArray = {}
	for i, monster in ipairs(GameData.rawData.monsters) do
		local chance = 0
		local weight = 0
		local minQty = 1
		local maxQty = 1
		if monster.bones ~= nil and monster.bones.itemID == item.id and Monsters._getMonsterBones(monster) ~= nil then
			-- Item is a bone, and is either a shard from God dungeons or dropped by a non-boss monster with a loot table
			maxQty = (monster.bones.quantity ~= nil and monster.bones.quantity) or 1
			minQty = maxQty
			chance = 1
			weight = 1
		elseif monster.barrierPercent ~= nil and 'melvorAoD:Barrier_Dust' == item.id and not Monsters._isDungeonOnlyMonster(monster) then
			-- Item is Barrier Dust and is not a dungeon exclusive monster
			maxQty = math.max(math.floor(Monsters._getMonsterStat(monster, 'Barrier') / 10 / 20), 1)
			minQty = maxQty
			chance = 1
		elseif monster.lootTable ~= nil and not Monsters._isDungeonOnlyMonster(monster) then
			-- If the monster has a loot table, check if the item we are looking for is in there
			-- Dungeon exclusive monsters don't count as they are either:
			--   - A monster before the boss, which doesn't drop anything except shards (checked above)
			--   - A boss monster, whose drops are accounted for in data from Areas instead
			for j, loot in ipairs(monster.lootTable) do
				weight = weight + loot.weight
				if loot.itemID == item.id then
					chance = loot.weight
					minQty = loot.minQuantity
					maxQty = loot.maxQuantity
				end
			end
			local lootChance = monster.lootChance ~= nil and (monster.bones == nil or monster.bones.itemID ~= item.id) and monster.lootChance or 100
			chance = chance * lootChance
			weight = weight * 100
			chance, weight = Num.fractionpair(chance, weight)
		end
		if chance > 0 then
			-- Item drops when the monster is killed
			table.insert(resultArray, {id = monster.id, dropWt = chance, totalWt = weight, minQty = minQty, maxQty = maxQty})
		end
	end
	return resultArray
end

function p.getItemMonsterSources(itemName)
	local item = Items.getItem(itemName)
	return p._getItemMonsterSources(item)
end

function p._getItemArchSources(item)
	local check = false
	local itemID = item.id
	
	local resultArray = {}
	
	for i, digSite in pairs(SkillData.Archaeology.digSites) do
		for sizeName, size in pairs(digSite.artefacts) do
			local found = nil
			local sizeWeight = 0
			for k, artefact in pairs(size) do
				sizeWeight = sizeWeight + artefact.weight
				if artefact.itemID == itemID then
					found = artefact
				end
			end
			
			if found ~= nil then
				local min = found.minQuantity
				local max = found.maxQuantity
				table.insert(resultArray, {
					id = digSite.id, 
					name = digSite.name, 
					level = digSite.level,
					size = sizeName, 
					minQty = min, 
					maxQty = max, 
					dropWt = found.weight, 
					totalWt = sizeWeight})
			end
		end
	end
	
	return resultArray
end

function p.getItemArchSources(itemName)
	local item = Items.getItem(itemName)
	return p._getItemArchSources(item)
end


-- Uncomment this block and execute 'p.test()' within the debug console
-- to test after making changes
function p.test()
	local checkItems = {
		-- "Circlet of Rhaelyx",
		-- "Jewel of Rhaelyx",
		-- "Signet Ring Half (a)",
		-- "Signet Ring Half (b)",
		-- "Astrology Lesser Relic",
		-- "Mysterious Stone",
		-- "Gold Topaz Ring",
		"Charcoal",
		"Ash",
		"Coal Ore",
		"Rune Essence",
		"Gold Bar",
		-- "Rune Platebody",
		"Arrow Shafts",
		-- "Yew Longbow",
		-- "Water Rune",
		"Steam Rune",
		-- "Wolf",
		"Fox",
		"Leprechaun",
		"Void Wisp",
		-- "Redwood Logs",
		-- "Shadow Raven Nest",
		"Raw Shrimp",
		"Shrimp",
		"Carrot Cake",
		-- "Carrot Cake (Perfect)",
		-- "Mantalyme Herb",
		"Carrot",
		"Controlled Heat Potion II",
		"Topaz",
		"Oricha",
		"Nightopal",
		-- "Sanguine Blade",
		-- "Ring of Power",
		-- "Infernal Claw",
		-- "Chapeau Noir",
		"Stardust",
		"Golden Stardust",
		"Abyssal Stardust",
		"Rope",
		"Ancient Ring of Mastery",
		"Mastery Token (Cooking)",
		"Gem Gloves",
		"Magic Bones",
		"Bowstring",
		"Superior Max Skillcape",
		"Thief's Moneysack",
		"Golden Star",
		-- "Slayer Deterer",
		"Paper",
		-- "Lemon",
		"Aranite Brush",
		"Barrier Dust",
		"Gloom Resin",
		"Gloom Amber",
		"Gloom Vine",
		"Gloom Vein Seed",
		"Elite Chest",
		"Abyssal Coin Contract II",
		"Dark Summon Consumable II",
	}
	local checkFuncs = {
		p.getItemSourceTables,
		--p.getCreationTable,
		--p.getItemSources,
		--p.getItemLootSourceTable,
	}
	local errCount = 0
	for i, item in ipairs(checkItems) do
		local param = {args={item}}
		mw.log('==' .. item .. '==')
		for j, func in ipairs(checkFuncs) do
			local callSuccess, retVal = pcall(func, param)
			if not callSuccess then
				errCount = errCount + 1
				mw.log('Error with item "' .. item .. '": ' .. retVal)
			else
				mw.log(retVal)
			end
		end
	end
	if errCount == 0 then
		mw.log('Test successful')
	else
		mw.log('Test failed with ' .. errCount .. ' failures')
	end
end


return p