Module:Items/SourceTables: Difference between revisions

From Melvor Idle
m (hello world)
(Add depths and stronghold source for ItemBox)
 
(133 intermediate revisions by 10 users not shown)
Line 1: Line 1:
local p = {}
local p = {}


local MonsterData = mw.loadData('Module:Monsters/data')
local Constants = require('Module:Constants')
local ItemData = mw.loadData('Module:Items/data')
local SkillData = mw.loadData('Module:Skills/data')
local Constants = mw.loadData('Module:Constants/data')
 
local Shared = require('Module:Shared')
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 Magic = require('Module:Magic')
local Areas = require('Module:CombatAreas')
local Icons = require('Module:Icons')
local Icons = require('Module:Icons')
local Items = require('Module:Items')
local Items = require('Module:Items')
local Shop = require('Module:Shop')
local Monsters = require('Module:Monsters')
local Skills = require('Module:Skills')
local Num = require('Module:Number')


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


function p._getCreationTable(item)
function p._getCreationTable(item)
  local skill = ''
local skill = ''
  local specialReq = nil
local specialReq = nil
  local time = 0
local time = 0
  local maxTime = nil
local maxTime = nil
  local lvl = 0
local lvl = 0
  local xp = 0
local isAbyssal = false
  local qty = nil
local xp = 0
  local req = nil
local qty = nil
  local result = ''
local req = nil
 
local tables = {}
local itemID = item.id
--First figure out what skill is used to make this...
 
local skillIDs = {
['Gathering'] = {
['Woodcutting'] = { recipeKey = 'trees' },
['Fishing'] = { recipeKey = 'fish' },
['Mining'] = { recipeKey = 'rockData' },
['Farming'] = { recipeKey = 'recipes' }
},
['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 lvl, isAbyssal, xp, qty, req, time, maxTime = 0, false, 0, 0, nil, 0, nil
for i, recipe in ipairs(skillData[dataProp.recipeKey]) do
if recipe.productId == itemID then
lvl, isAbyssal = Skills.getRecipeLevelRealm(localSkillID, recipe)
xp = recipe.baseAbyssalExperience or recipe.baseExperience
qty = recipe.baseQuantity or 1
if localSkillID == 'Farming' then
req = { recipe.seedCost }
local category = GameData.getEntityByID(skillData.categories, recipe.categoryID)
qty = 5 * category.harvestMultiplier
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
-- Special requirements
if recipe.totalMasteryRequired ~= nil then
specialReq = Icons.Icon({'Mastery', notext=true}) .. Num.formatnum(recipe.totalMasteryRequired) .. ' total [[' .. skill .. ']] [[Mastery]]'
end
table.insert(tables, p.buildCreationTable(skill, lvl, isAbyssal, xp, req, qty, time, maxTime, specialReq))
-- Assumes item has a single source per skill
break
end
end
end


  local tables = {}
-- Artisan skills
  --First figure out what skill is used to make this...
-- Allow follow a similar data structure
  if item.smithingLevel ~= nil then
for localSkillID, dataProp in pairs(skillIDs.Artisan) do
    skill = 'Smithing'
local skillData = SkillData[localSkillID]
    lvl = item.smithingLevel
local skill = skillData.name
    xp = item.smithingXP
local lvl, isAbyssal, xp, qty, req, time, maxTime = 0, false, 0, 0, nil, 0, nil
    req = item.smithReq
for i, recipe in ipairs(skillData.recipes) do
    qty = item.smithingQty
if recipe.productID == itemID or
    time = 2
(localSkillID == 'Cooking' and recipe.perfectCookID == itemID) or
    table.insert(tables, p.buildCreationTable(skill, lvl, xp, req, qty, time))
(localSkillID == 'Herblore' and Shared.contains(recipe.potionIDs, itemID)) then
  end
lvl, isAbyssal = Skills.getRecipeLevelRealm(localSkillID, recipe)
  if item.craftingLevel ~= nil then
xp = recipe.baseAbyssalExperience or recipe.baseExperience
    skill = 'Crafting'
qty = recipe.baseQuantity or 1
    lvl = item.craftingLevel
-- Action time
    xp = item.craftingXP
if recipe.baseMinInterval ~= nil then
    req = item.craftReq
time = recipe.baseMinInterval / 1000
    qty = item.craftQty
if recipe.baseMaxInterval ~= nil then
    time = 3
maxTime = recipe.baseMaxInterval / 1000
    table.insert(tables, p.buildCreationTable(skill, lvl, xp, req, qty, time))
end
  end
elseif recipe.baseInterval ~= nil then
  if item.runecraftingLevel ~= nil then
time = recipe.baseInterval /1000
    skill = 'Runecrafting'
elseif skillData.baseInterval ~= nil then
    lvl = item.runecraftingLevel
time = skillData.baseInterval / 1000
    xp = item.runecraftingXP
end
    req = item.runecraftReq
-- Special requirements
    qty = item.runecraftQty
-- Potions have a mastery level requirement depending on the tier
    time = 2
if item.charges ~= nil and item.tier ~= nil then
    table.insert(tables, p.buildCreationTable(skill, lvl, xp, req, qty, time))
local levelUnlock = GameData.getEntityByProperty(skillData.masteryLevelUnlocks, 'descriptionID', item.tier + 1)
  end
if levelUnlock ~= nil then
  if item.fletchingLevel ~= nil then
specialReq = Icons._MasteryReq(item.name, levelUnlock.level, false)
    skill = 'Fletching'
end
    lvl = item.fletchingLevel
end
    xp = item.fletchingXP
-- Materials & output quantity
    req = item.fletchReq
-- Special case for Summoning recipes
    qty = item.fletchQty
if localSkillID == 'Summoning' then
    time = 2
local shardCostArray, otherCostArray = {}, {}
    if item.name == 'Arrow Shafts' then
local recipeCost = 0
      --Arrow Shafts get special (weird) treatment
if isAbyssal == true then
      req = '1 of any [[Log]]'
recipeCost = skillData.recipeAPCost
      qty = '15 - 135'
else
    end
recipeCost = skillData.recipeGPCost
    table.insert(tables, p.buildCreationTable(skill, lvl, xp, req, qty, time))
end
  end
-- Shards
  if item.herbloreReq ~= nil then
for j, itemCost in ipairs(recipe.itemCosts) do
    skill = 'Herblore'
local shard = Items.getItemByID(itemCost.id)
    req = item.herbloreReq
if shard ~= nil then
    --Currently using 'herbloreMasteryID' as shorthand to find details, could be a better method
table.insert(shardCostArray, Icons.Icon({shard.name, type='item', notext=true, qty=itemCost.quantity}))
    local potionID = item.herbloreMasteryID
end
    local potionData = SkillData.Herblore.ItemData[potionID + 1]
end
    lvl = potionData.herbloreLevel
-- Other costs
    xp = potionData.herbloreXP
table.insert(otherCostArray, Common.getCostString({ ["items"] = {}, ["currencies"] = recipe.currencyCosts}))
    time = 2
for j, nonShardID in ipairs(recipe.nonShardItemCosts) do
    table.insert(tables, p.buildCreationTable(skill, lvl, xp, req, qty, time))
local nonShard = Items.getItemByID(nonShardID)
  end
if nonShard ~= nil then
  if item.miningID ~= nil then
local itemValue = math.max(nonShard.sellsFor, 20)
    skill = 'Mining'
local nonShardQty = math.max(1, math.ceil(recipeCost / itemValue))
    lvl = SkillData.Mining.Rocks[item.masteryID[2] + 1].level
table.insert(otherCostArray, Icons.Icon({nonShard.name, type='item', notext=true, qty=nonShardQty}))
    time = 3
end
    xp = item.miningXP
end
    --Rune Essence has double quantity, but that's a hard-coded thing in the game so it's hard-coded here
req = table.concat(shardCostArray, ', ')
    if item.name == 'Rune Essence' then qty = 2 else qty = 1 end
if not Shared.tableIsEmpty(otherCostArray) then
local costLen = Shared.tableCount(otherCostArray)
req = req .. '<br/>' .. (costLen == 1 and '' or 'and one of the following:<br/>') .. table.concat(otherCostArray, "<br/>'''OR''' ")
end
specialReq = 'At least 1 ' .. Icons.Icon({'Summoning%23Summoning Marks', item.name, img=item.name, type='mark'}) .. ' mark discovered'
table.insert(tables, p.buildCreationTable(skill, lvl, isAbyssal, xp, req, qty, time, nil, specialReq))
-- 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 reqSubPart = {}
for k, itemCost in ipairs(altCost.itemCosts) do
local reqItem = Items.getItemByID(itemCost.id)
if reqItem == nil then
table.insert(reqSubPart, itemCost.quantity .. 'x ?????')
else
table.insert(reqSubPart, Icons.Icon({reqItem.name, type='item', qty=itemCost.quantity}))
end
end


    if item.name == 'Dragonite Ore' then
table.insert(reqSubPart, Common.getCostString({ ["items"] = {}, ["currencies"] = recipe.currencyCosts}))
      specialReq = Icons.Icon({"Mastery", notext='true'})..' 271 total [[Mining]] [[Mastery]]'
table.insert(reqPart, table.concat(reqSubPart, ', '))
    end
table.insert(qtyPart, Num.formatnum(qty * altCost.quantityMultiplier))
    table.insert(tables, p.buildCreationTable(skill, lvl, xp, req, qty, time, nil, specialReq))
end
  end
local sep = "<br/>'''OR''' "
  if item.type == "Logs" then
req = table.concat(reqPart, sep)
    --Well this feels like cheating, but for as long as logs are the first items by ID it works
local qtyText = table.concat(qtyPart, sep)
    local treeData = SkillData.Woodcutting.Trees[item.id + 1]
table.insert(tables, p.buildCreationTable(skill, lvl, isAbyssal, xp, req, qtyText, time, maxTime, specialReq))
    skill = 'Woodcutting'
-- Finally, normal recipes with a single set of item costs
    lvl = treeData.level
elseif type(recipe.itemCosts) == 'table' and not Shared.tableIsEmpty(recipe.itemCosts) then
    time = treeData.interval / 1000
table.insert(tables, p.buildCreationTable(skill, lvl, isAbyssal, xp, recipe.itemCosts, qty, time, maxTime, specialReq, recipe.currencyCosts))
    xp = treeData.xp
end
    table.insert(tables, p.buildCreationTable(skill, lvl, xp, req, qty, time))
end
  end
end
  if item.fishingLevel ~= nil then
end
    skill = 'Fishing'
    lvl = item.fishingLevel
    xp = item.fishingXP
    time = item.minFishingInterval/1000
    maxTime = item.maxFishingInterval/1000
    table.insert(tables, p.buildCreationTable(skill, lvl, xp, req, qty, time, maxTime))
  end
  if item.type == "Havest" or item.type == "Herb" or item.type == "Logs" then
    --Havest/Herb means farming
    --Logs might mean farming or might not. Depends on the logs
    --Yes, Havest. The typos are coming from inside the source code
    for i, item2 in pairs(ItemData.Items) do
      if item2.grownItemID == item.id then
        skill = 'Farming'
        lvl = item2.farmingLevel
        xp = item2.farmingXP
        time = item2.timeToGrow
        qty = 5
        req = {{id = i - 1, qty = (item2.seedsRequired ~= nil and item2.seedsRequired or 1)}}
        table.insert(tables, p.buildCreationTable(skill, lvl, xp, req, qty, time))
        break
      end
    end
  end
  if item.type == "Food" or item.type == "Cooked Fish" then
    --Food/Cooked Fish is Fishing, need to figure out source item
    for i, item2 in pairs(ItemData.Items) do
      if item2.burntItemID == item.id or item2.cookedItemID == item.id then
        skill = 'Cooking'
        lvl = item2.cookingLevel
        if item2.burntItemID == item.id then
          xp = 1
        else
          xp = item2.cookingXP
        end
        time = 3
        req = {{id = i - 1, qty = 1}}
        break
      end
    end
    if skill ~= '' then
      table.insert(tables, p.buildCreationTable(skill, lvl, xp, req, qty, time))
    end
  end
  --A couple special exceptions for Alt Magic
  --Not Gems or Bars though since those have their own separate thing
  if item.name == 'Rune Essence' then
    table.insert(tables, p.buildAltMagicTable('Just Learning'))
  elseif item.name == 'Bones' then
    table.insert(tables, p.buildAltMagicTable('Bone Offering')) 
  elseif item.name == 'Holy Dust' then
    table.insert(tables, p.buildAltMagicTable('Blessed Offering'))
  end


  if Shared.tableCount(tables) == 0 then
-- Alt. Magic, excludes spells which can produce a variety of items, such as Gems and Bars
    return ""
-- Bars are handled by getItemSuperheatTable()
  else
-- Gems are handled by _getItemLootSourceTable()
    return table.concat(tables, '\r\n')
for i, altSpell in ipairs(Magic.getSpellsBySpellBook('altMagic')) do
  end
if altSpell.produces == item.id then
table.insert(tables, p._buildAltMagicTable(altSpell))
end
end
 
if Shared.tableIsEmpty(tables) then
return ''
else
return table.concat(tables, '\r\n')
end
end
end


function p.buildAltMagicTable(spellName)
function p.getAltMagicTable(frame)
  local spell = Magic.getSpell(spellName, 'AltMagic')
local spellName = frame.args ~= nil and frame.args[1] or frame
  local result = '{|class="wikitable"\r\n|-'
local spell = Magic.getSpell(spellName, 'altMagic')
  result = result..'\r\n!colspan="2"|'..Icons.Icon({spell.name, type='spell'})
if spell == nil then
  result = result..'\r\n|-\r\n!style="text-align:right;"|Requirements'
return Shared.printError('Could not find Alt. Magic spell "' .. spellName .. '"')
  result = result..'\r\n|'..Icons._SkillReq('Magic', spell.magicLevelRequired)
else
  -- 1 means select any item. 0 would mean Superheat, but that's handled elsewhere
return p._buildAltMagicTable(spell)
  -- -1 means no item is needed, so hide this section
end
  if spell.selectItem == 1 then
end
    result = result..'\r\n|-\r\n!style="text-align:right;"|Materials'
    result = result..'\r\n|1 of any item'
  end
  --Add runes
  result = result..'\r\n|-\r\n!style="text-align:right;"|Runes\r\n|'
  for i, req in pairs(spell.runesRequired) do
    local rune = Items.getItemByID(req.id)
    if i > 1 then result = result..', ' end
    result = result..Icons.Icon({rune.name, type='item', notext=true, qty=req.qty})
  end
  if spell.runesRequiredAlt ~= nil and Shared.tableCount(spell.runesRequired) ~= Shared.tableCount(spell.runesRequiredAlt) then
    result = result.."<br/>'''OR'''<br/>"
    for i, req in pairs(spell.runesRequiredAlt) do
      local rune = Items.getItemByID(req.id)
      if i > 1 then result = result..', ' end
      result = result..Icons.Icon({rune.name, type='item', notext=true, qty=req.qty})
    end
  end


  --Now just need the output quantity, xp, and casting time (which is always 2)
function p._buildAltMagicTable(spell)
  result = result..'\r\n|-\r\n!style="text-align:right;"|Base Quantity\r\n|'..spell.convertToQty
local resultPart = {}
  result = result..'\r\n|-\r\n!style="text-align:right;"|Base XP\r\n|'..spell.magicXP
local imgType = Magic._getSpellIconType(spell)
  result = result..'\r\n|-\r\n!style="text-align:right;"|Cast Time\r\n|2s'
table.insert(resultPart, '{|class="wikitable"\r\n|-')
  result = result..'\r\n|}'
table.insert(resultPart, '\r\n!colspan="2"|'..Icons.Icon({spell.name, type=imgType}))
  return result
table.insert(resultPart, '\r\n|-\r\n!style="text-align:right;"|Requirements')
table.insert(resultPart, '\r\n|'..Icons._SkillReq('Magic', spell.level))
 
local costText = Magic._getAltSpellCostText(spell)
if costText ~= nil then
table.insert(resultPart, '\r\n|-\r\n!style="text-align:right;"|Materials')
table.insert(resultPart, '\r\n| ' .. costText)
end
 
--Add runes
table.insert(resultPart, '\r\n|-\r\n!style="text-align:right;"|Runes\r\n| ' .. Magic._getSpellRunes(spell))
 
--Now just need the output quantity, xp, and casting time (which is always 2)
table.insert(resultPart, '\r\n|-\r\n!style="text-align:right;"|Base Quantity\r\n|' .. spell.productionRatio)
table.insert(resultPart, '\r\n|-\r\n!style="text-align:right;"|Base XP\r\n|' .. spell.baseExperience)
table.insert(resultPart, '\r\n|-\r\n!style="text-align:right;"|Cast Time\r\n|2s')
table.insert(resultPart, '\r\n|}')
return table.concat(resultPart)
end
end


function p.buildCreationTable(skill, lvl, xp, req, qty, time, maxTime, specialReq)
function p.buildCreationTable(skill, lvl, isAbyssal, xp, req, qty, time, maxTime, specialReq, currencyCost)
  if qty == nil then qty = 1 end
if qty == nil then qty = 1 end
  local result = '{|class="wikitable"'
local resultPart = {}
  if req ~= nil then
table.insert(resultPart, '{|class="wikitable"')
    result = result..'\r\n!colspan="2"|Item Creation'
table.insert(resultPart, '\r\n!colspan="2"|Item ' .. (req == nil and 'Creation' or 'Production'))
  else
table.insert(resultPart, '\r\n|-\r\n!style="text-align: right;"|Requirements')
    result = result..'\r\n!colspan="2"|Item Production'
table.insert(resultPart, '\r\n|'..Icons._SkillReq(skill, lvl, false, (isAbyssal and "melvorItA:Abyssal" or nil)))
  end
if specialReq ~= nil then table.insert(resultPart, '<br/>'..specialReq) end
  result = result..'\r\n|-\r\n!style="text-align: right;"|Requirements'
  result = result..'\r\n|'..Icons._SkillReq(skill, lvl)
  if specialReq ~= nil then result = result..'<br/>'..specialReq end


  if req ~= nil then  
if req ~= nil then
    result = result..'\r\n|-\r\n!style="text-align: right;"|Materials\r\n|'
table.insert(resultPart, '\r\n|-\r\n!style="text-align: right;"|Materials\r\n|')
    if type(req) == 'table' then
if type(req) == 'table' then
      for i, mat in pairs(req) do
for i, mat in ipairs(req) do
        if i > 1 then result = result..'<br/>' end
if i > 1 then table.insert(resultPart, '<br/>') end
        local matItem = Items.getItemByID(mat.id)
local matItem = Items.getItemByID(mat.id)
        if matItem == nil then
if matItem == nil then
          result = result..mat.qty..'x ?????'
table.insert(resultPart, mat.quantity..'x ?????')
        else
else
          result = result..Icons.Icon({matItem.name, type='item', qty=mat.qty})
table.insert(resultPart, Icons.Icon({matItem.name, type='item', qty=mat.quantity}))
        end
end
      end
end
    else
    result = result..req
if currencyCost ~= nil then
    end
table.insert(resultPart, Common.getCostString({ ["items"] = {}, ["currencies"] = currencyCost }))
  end
end
  result = result..'\r\n|-\r\n!style="text-align:right;"|Base Quantity'
else
  result = result..'\r\n|'..qty
table.insert(resultPart, req)
  result = result..'\r\n|-\r\n!style="text-align:right;"|Base Experience'
end
  result = result..'\r\n|'..Shared.formatnum(xp)..' XP'
end
  result = result..'\r\n|-\r\n!style="text-align:right;"|Base Creation Time'
table.insert(resultPart, '\r\n|-\r\n!style="text-align:right;"|Base Quantity')
  result = result..'\r\n|'..Shared.formatnum(Shared.round(time, 2, 0))..'s'
table.insert(resultPart, '\r\n|'..qty)
  if maxTime ~= nil then result = result..' - '..Shared.formatnum(Shared.round(maxTime, 2, 0))..'s' end
table.insert(resultPart, '\r\n|-\r\n!style="text-align:right;"|Base Experience')
  result = result..'\r\n|}'
table.insert(resultPart, '\r\n|'..Num.formatnum(xp)..' XP')
table.insert(resultPart, '\r\n|-\r\n!style="text-align:right;"|Base Creation Time')
table.insert(resultPart, '\r\n|'..Shared.timeString(time, true))
if maxTime ~= nil and maxTime > time then table.insert(resultPart, ' - '..Shared.timeString(maxTime, true)) end
table.insert(resultPart, '\r\n|}')


  return result
return table.concat(resultPart)
end
end


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


function p._getItemSources(item, asList, addCategories)
function p._getItemSources(item, asList, addCategories, separator)
   local result = nil
local lineArray = {}
  local lineArray = {}
local categoryArray = {}
  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
local iconType = entity == 'Dungeon' and 'dungeon' or 'combatArea'
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=iconType, 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=iconType, 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


  --Alright, time to go through all the ways you can get an item...
local upgradeCat = false
  --First up: Can we kill somebody and take theirs?
for catName, parts in pairs(upgradePart) do
  local killStr = ''
if not Shared.tableIsEmpty(parts) then
  local dungeonStr = ''
if not upgradeCat then
  local count1 = 0
table.insert(categoryArray, '[[Category:Upgraded Items]]')
  for i, monster in Shared.skpairs(MonsterData.Monsters) do
upgradeCat = true
    local isDrop = false
end
    if monster.bones == item.id and ((monster.lootTable ~= nil and not monster.isBoss) or Shared.contains(item.name, "Shard")) then
local typeText = (catName == 'up' and 'Upgrading') or 'Downgrading'
      isDrop = true
table.insert(lineArray, typeText .. ': ' .. table.concat(parts, sep))
    elseif monster.lootTable ~= nil then
end
      for j, loot in pairs(monster.lootTable) do
end
        if loot[1] == item.id then
          isDrop = true
        end
      end
    end
    if isDrop then
      if monster.isBoss then
        local areaList = Areas.getMonsterAreas(i - 1)
        --If this is a boss then we actually are completing dungeons for this and need to figure out which one
        for j, dung in pairs(areaList) do
          if string.len(dungeonStr) > 0 then
            dungeonStr = dungeonStr..','
          else
            dungeonStr = 'Completing: '
          end
          dungeonStr = dungeonStr..Icons.Icon({dung.name, type="dungeon", notext=true})
        end
      else
        count1 = count1 + 1
        if string.len(killStr) > 0 then
          killStr = killStr..','
          if count1 % 3 == 1 and count1 > 1 then killStr = killStr..'<br/>' end
          killStr = killStr..Icons.Icon({monster.name, type="monster", notext="true"})
        else
          killStr = killStr..'Killing: '..Icons.Icon({monster.name, type="monster", notext="true"})
        end
      end
    end
  end
  --Add the Fire Cape's special exception:
  if item.name == 'Fire Cape' then
    if string.len(dungeonStr) > 0 then
      dungeonStr = dungeonStr..','
    else
      dungeonStr = 'Completing: '
    end
    dungeonStr = dungeonStr..Icons.Icon({"Volcanic Cave", type="dungeon", notext=true})
  elseif item.name == 'Infernal Cape' then
    if string.len(dungeonStr) > 0 then
      dungeonStr = dungeonStr..','
    else
      dungeonStr = 'Completing: '
    end
    dungeonStr = dungeonStr..Icons.Icon({"Infernal Stronghold", type="dungeon", notext=true})
  end


  if string.len(dungeonStr) > 0 then table.insert(lineArray, dungeonStr) end
--Next: Can we take it from somebody else -without- killing them?
  if string.len(killStr) > 0 then table.insert(lineArray, killStr) end
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


  --Next: Can we find it in a box?
--Check if we can make it ourselves
  --While we're here, check for upgrades, cooking, and growing
local skillIDs = {
  local lootStr = ''
['Gathering'] = {
  local upgradeStr = ''
['Woodcutting'] = { recipeKey = 'trees' },
  local cookStr = ''
['Fishing'] = { recipeKey = 'fish' },
  local burnStr = ''
['Mining'] = { recipeKey = 'rockData' },
  local growStr = ''
['Farming'] = { recipeKey = 'recipes' }
  local count2 = 0
},
  count1 = 0
['Artisan'] = {
  for i, item2 in pairs(ItemData.Items) do
['Cooking'] = { },
    if item2.dropTable ~= nil then
['Smithing'] = { },
      for j, loot in pairs(item2.dropTable) do
['Fletching'] = { },
        if loot[1] == item.id then
['Crafting'] = { },
          count1 = count1 + 1
['Runecrafting'] = { },
          if string.len(lootStr) > 0 then
['Herblore'] = { },
            lootStr = lootStr..','
['Summoning'] = { }
            if count1 % 3 == 1 and count1 > 1 then lootStr = lootStr..'<br/>' end
}
            lootStr = lootStr..Icons.Icon({item2.name, type="item", notext="true"})
}
          else
            lootStr = lootStr..'Opening: '..Icons.Icon({item2.name, type="item", notext="true"})
          end
        end
      end
    end
    if item2.trimmedItemID == item.id then
          count2 = count2 + 1
        if string.len(upgradeStr) > 0 then
          upgradeStr = upgradeStr..','
          if count2 % 3 == 1 and count2 > 1 then upgradeStr = upgradeStr..'<br/>' end
          upgradeStr = upgradeStr..Icons.Icon({item2.name, type="item", notext="true"})
        else
          table.insert(categoryArray, '[[Category:Upgraded Items]]')
          upgradeStr = upgradeStr..'Upgrading: '..Icons.Icon({item2.name, type="item", notext="true"})
        end
    end
    if item2.cookedItemID == item.id then
        if string.len(cookStr) > 0 then
          cookStr = cookStr..','..Icons.Icon({item2.name, type="item", notext="true"})
        else
          table.insert(categoryArray, '[[Category:Cooked Items]]')
          cookStr = cookStr..'Cooking: '..Icons.Icon({item2.name, type="item", notext="true"})
        end
    end
    if item2.burntItemID == item.id then
        if string.len(burnStr) > 0 then
          burnStr = burnStr..','..Icons.Icon({item2.name, type="item", notext="true"})
        else
          table.insert(categoryArray, '[[Category:Burnt Items]]')
          burnStr = burnStr..'Burning: '..Icons.Icon({item2.name, type="item", notext="true"})
        end
    end
    if item2.grownItemID == item.id then
        if string.len(growStr) > 0 then
          growStr = growStr..','..Icons.Icon({item2.name, type="item", notext="true"})
        else
          table.insert(categoryArray, '[[Category:Harvestable Items]]')
          growStr = growStr..'Growing: '..Icons.Icon({item2.name, type="item", notext="true"})
        end
    end
  end
  if string.len(lootStr) > 0 then table.insert(lineArray, lootStr) end
  if string.len(upgradeStr) > 0 then table.insert(lineArray, upgradeStr) end
  if string.len(cookStr) > 0 then table.insert(lineArray, cookStr) end
  if string.len(burnStr) > 0 then table.insert(lineArray, burnStr) end
  if string.len(growStr) > 0 then table.insert(lineArray, growStr) end


  --Next: Can we take it from somebody else -without- killing them?
-- Gathering skills
  local thiefStr = ''
for localSkillID, dataProp in pairs(skillIDs.Gathering) do
  for i, npc in pairs(SkillData.Thieving) do
local skillData = SkillData[localSkillID]
    if npc.lootTable ~= nil then
local skill = skillData.name
      for j, loot in pairs(npc.lootTable) do
for i, recipe in ipairs(skillData[dataProp.recipeKey]) do
        if loot[1] == item.id then
if recipe.productId == item.id then
          if string.len(thiefStr) > 0 then
if localSkillID == 'Farming' and recipe.seedCost ~= nil then
            thiefStr = thiefStr..','..Icons.Icon({npc.name, type="thieving", notext="true"})
local seedItem = Items.getItemByID(recipe.seedCost.id)
          else
if seedItem ~= nil then
            thiefStr = thiefStr..'Pickpocketing: '..Icons.Icon({npc.name, type="thieving", notext="true"})
table.insert(lineArray, 'Growing: ' .. Icons.Icon({seedItem.name, type='item', notext='true'}))
          end
end
        end
else
      end
local level, isAbyssal = Skills.getRecipeLevelRealm(localSkillID, recipe)
    end
table.insert(lineArray, Icons._SkillReq(skill, level, false, (isAbyssal and "melvorItA:Abyssal" or nil)))
  end
end
  if string.len(thiefStr) > 0 then table.insert(lineArray, thiefStr) end
break
end
end
end


  --If all else fails, I guess we should check if we can make it ourselves
-- Artisan skills
  --SmithCheck:
for localSkillID, dataProp in pairs(skillIDs.Artisan) do
  if item.smithingLevel ~= nil then
local skillData = SkillData[localSkillID]
    table.insert(lineArray, Icons._SkillReq("Smithing", item.smithingLevel))
local skill = skillData.name
  end
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


  --CraftCheck:
-- Township trading
  if item.craftingLevel ~= nil then
for i, tsResource in ipairs(SkillData.Township.itemConversions.fromTownship) do
    table.insert(lineArray, Icons._SkillReq("Crafting", item.craftingLevel))
local found = false
  end
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


  --FletchCheck:
-- Archaeology sources
  if item.fletchingLevel ~= nil then
-- Digsites
    table.insert(lineArray, Icons._SkillReq("Fletching", item.fletchingLevel))
for i, digsite in ipairs(SkillData.Archaeology.digSites) do
  end
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


  --RunecraftCheck:
-- Cartography
  if item.runecraftingLevel ~= nil then
-- Paper
    table.insert(lineArray, Icons._SkillReq("Runecrafting", item.runecraftingLevel))
for i, recipe in ipairs(SkillData.Cartography.paperRecipes) do
  end
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


  --MineCheck:
--AstrologyCheck
  if item.miningID ~= nil then
for i, dustDrop in ipairs(SkillData.Astrology.baseRandomItemChances) do
    table.insert(lineArray, Icons._SkillReq("Mining", SkillData.Mining.Rocks[item.masteryID[2] + 1].level))
if dustDrop.itemID == item.id then
  end
table.insert(lineArray, Icons.Icon({SkillData.Astrology.name, type='skill'}))
end
end


  --FishCheck:
-- Woodcutting
  if (item.category == "Fishing" and (item.type == "Junk" or item.type == "Special")) then
-- Raven Nest
    table.insert(lineArray, Icons.Icon({"Fishing", type='skill', notext=true})..' [[Fishing#'..item.type..'|'..item.type..']]')
if item.id == SkillData.Woodcutting.ravenNestItemID then
  elseif item.fishingLevel ~= nil then
local levelReq = nil
    table.insert(lineArray, Icons._SkillReq("Fishing", item.fishingLevel))
for i, tree in ipairs(SkillData.Woodcutting.trees) do
  end
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


  --HerbCheck:
-- Fishing
  if item.herbloreMasteryID ~= nil then
-- Junk
    local potionData = SkillData.Herblore.ItemData[item.herbloreMasteryID + 1].herbloreLevel
if Shared.contains(SkillData.Fishing.junkItemIDs, item.id) then
    table.insert(lineArray, Icons._SkillReq("Herblore", potionData))
table.insert(lineArray, Icons.Icon({'Fishing', type='skill', notext=true}) .. ' [[Fishing#Junk|Junk]]')
  end
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


  --WoodcuttingCheck
-- Firemaking: Coal
  if item.type == 'Logs' then
if Shared.contains({SkillData.Firemaking.coalItemID,
    local treeData = SkillData.Woodcutting.Trees[item.id + 1]
SkillData.Firemaking.ashItemID,
    local lvl = treeData.level
SkillData.Firemaking.charcoalItemID,
    table.insert(lineArray, Icons._SkillReq("Woodcutting", lvl))
SkillData.Firemaking.fireSpiritItemID,
  end
SkillData.Firemaking.diamondItemID
}, item.id) then
table.insert(lineArray, Icons._SkillReq(SkillData.Firemaking.name, 1))
end


  --Finally there are some weird exceptions:
-- Mining: Gems
  --Coal can be acquired via firemaking
if (GameData.getEntityByProperty('randomGems', 'itemID', item.id) ~= nil or
  if item.name == "Coal Ore" then
GameData.getEntityByProperty('randomSuperiorGems', 'itemID', item.id) ~= nil) then
    table.insert(lineArray, Icons._SkillReq("Firemaking", 1))
table.insert(lineArray, Icons.Icon({"Mining", type='skill', notext=true})..' [[Mining#Gems|Gem]]')
  end
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


  --Gems can be acquired from mining, fishing, and alt. magic
-- General rare drops for non-combat skills
  if item.type == 'Gem' then
-- Includes items like Circlet/Jewel of Rhaelyx, Mysterious stones, Signet ring half (a),
    table.insert(lineArray, Icons.Icon({"Fishing", type='skill', notext=true})..' [[Fishing#Special|Special]]')
-- relics (for Ancient Relics mode)
    table.insert(lineArray, Icons.Icon({"Mining", type='skill', notext=true})..' [[Mining#Gems|Gem]]')
local skillIconList, subText = {}, ''
    table.insert(lineArray, Icons.Icon({"Alt. Magic", type='skill'}))
for i, skillDataAll in ipairs(GameData.rawData.skillData) do
  end
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


  --Bars and some other stuff can also be acquired via Alt. Magic
-- Supplementary stuff on top of general rare drops
  if type == 'Bar' or Shared.contains(AltMagicProducts, item.name) then
if item.id == 'melvorD:Gold_Topaz_Ring' then
    table.insert(lineArray, Icons.Icon({"Alt. Magic", type='skill'}))
table.insert(lineArray, 'Killing any monster if not worn (Instead of '..Icons.Icon({"Signet Ring Half (b)", type="item"})..')')
  end
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


  --Chapeau Noir & Bobby's Pocket are special Thieving items
--Tokens are from the appropriate skill
  if item.name == "Chapeau Noir" or item.name == "Bobby&apos;s Pocket" then
if item.modifiers ~= nil and item.modifiers.masteryToken ~= nil then
    table.insert(lineArray, Icons._SkillReq("Thieving", 1))
for localSkillID, skillData in pairs(SkillData) do
  end
if skillData.masteryTokenID ~= nil and skillData.masteryTokenID == item.id then
table.insert(lineArray, Icons._SkillReq(skillData.name, 1))
break
end
end
end


  --Rhaelyx pieces are also special
-- Golbin Raid exclusive items
  if item.name == 'Jewel of Rhaelyx' then
if item.golbinRaidExclusive then
    local rhaStr = 'Any action in: '
table.insert(lineArray, Icons.Icon({'Golbin Raid', type='pet', img='Golden Golbin'}))
    rhaStr = rhaStr..Icons.Icon({'Firemaking', type = 'skill', notext = true})..', '..Icons.Icon({'Cooking', type = 'skill', notext = true})..', '..Icons.Icon({'Smithing', type = 'skill', notext = true})..',<br/>'
end
    rhaStr = rhaStr..Icons.Icon({'Fletching', type = 'skill', notext = true})..', '..Icons.Icon({'Crafting', type = 'skill', notext = true})..', '..Icons.Icon({'Runecrafting', type = 'skill', notext = true})..',<br/>'
    rhaStr = rhaStr..Icons.Icon({'Herblore', type='skill'})
    table.insert(lineArray, rhaStr)
  elseif item.name == 'Circlet of Rhaelyx' then
    local rhaStr = 'Any action in: '
    rhaStr = rhaStr..Icons.Icon({'Woodcutting', type = 'skill', notext = true})..', '..Icons.Icon({'Fishing', type = 'skill', notext = true})..', '..Icons.Icon({'Mining', type = 'skill', notext = true})..',<br/>'
    rhaStr = rhaStr..Icons.Icon({'Thieving', type = 'skill', notext = true})..', '..Icons.Icon({'Farming', type = 'skill', notext = true})
    table.insert(lineArray, rhaStr)
  elseif item.name == 'Mysterious Stone' then
    local rhaStr = 'Any action in: '
    rhaStr = rhaStr..Icons.Icon({'Firemaking', type = 'skill', notext = true})..', '..Icons.Icon({'Cooking', type = 'skill', notext = true})..', '..Icons.Icon({'Smithing', type = 'skill', notext = true})..',<br/>'
    rhaStr = rhaStr..Icons.Icon({'Fletching', type = 'skill', notext = true})..', '..Icons.Icon({'Crafting', type = 'skill', notext = true})..', '..Icons.Icon({'Runecrafting', type = 'skill', notext = true})..',<br/>'
    rhaStr = rhaStr..Icons.Icon({'Herblore', type='skill', notext = true})..', '..Icons.Icon({'Woodcutting', type = 'skill', notext = true})..', '..Icons.Icon({'Fishing', type = 'skill', notext = true})..',<br/>'
    rhaStr = rhaStr..Icons.Icon({'Mining', type = 'skill', notext = true})..', '..Icons.Icon({'Thieving', type = 'skill', notext = true})..', '..Icons.Icon({'Farming', type = 'skill', notext = true})
    rhaStr = rhaStr..'<br/>after finding '..Icons.Icon({'Crown of Rhaelyx', type='item'})
    table.insert(lineArray, rhaStr)
  end
  --Tokens are from the appropriate skill
  if item.isToken then
    for skill, id in pairs(Constants.skill) do
      if id == item.skill then
        table.insert(lineArray, Icons._SkillReq(skill, 1))
      end
    end
  end


  --Shop items (including special items like gloves that aren't otherwise listed)
--Shop items (including special items like gloves that aren't otherwise listed)
  if item.slayerCost ~= nil or item.buysFor ~= nil or Shared.contains(Items.OtherShopItems, item.name) then
if not Shared.tableIsEmpty(Shop.getItemSourceArray(item.id)) then
    table.insert(lineArray, '[[Shop]]')
table.insert(lineArray, Icons.Icon({'Shop'}))
  end
end


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


  local result = ''
-- Township Task reward
  if asList then
for _, task in ipairs(SkillData.Township.tasks) do
    result = '* '..table.concat(lineArray, "\r\n* ")
if task.rewards.items[1] ~= nil then -- Skip tasks with no items
  else
if GameData.getEntityByID(task.rewards.items, item.id) then
    result = table.concat(lineArray, "<br/>")
table.insert(lineArray, Icons.Icon({'Tasks', type='township'}))
    result = '<div style="max-width:180px;text-align:right">'..result..'</div>'
break
  end
end
  if addCategories then result = result..table.concat(categoryArray, '') end
end
  return result
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
end


function p.getItemSources(frame)
function p.getItemSources(frame)
  local itemName = frame.args ~= nil and frame.args[1] or frame
local itemName = frame.args ~= nil and frame.args[1] or frame
  local item = Items.getItem(itemName)
local item = Items.getItem(itemName)
  local asList = false
local asList = false
  local addCategories = false
local addCategories = false
  if frame.args ~= nil then  
if frame.args ~= nil then
    asList = frame.args.asList ~= nil and frame.args.asList ~= '' and frame.args.asList ~= 'false'
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'
addCategories = frame.args.addCategories ~= nil and frame.args.addCategories ~= '' and frame.args.addCategories ~= 'false'
  end
end
  if item == nil then
if item == nil then
    return "ERROR: No item named "..itemName.." exists in the data module"
return Shared.printError('No item named "' .. itemName .. '" exists in the data module')
  end
end


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


function p._getItemLootSourceTable(item)
function p._getItemLootSourceTable(item)
  local result = '{| class="wikitable sortable stickyHeader"'
local resultPart = {}
  result = result..'\r\n|- class="headerRow-0"'
table.insert(resultPart, '{| class="wikitable sortable stickyHeader"')
  result = result..'\r\n!Source!!Source Type!!Quantity!!Chance'
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
--Set up function for adding rows
  local buildRow = function(source, type, minqty, qty, chance)
local buildRow = function(source, level, levelNum, minqty, qty, weight, totalWeight, expIcon)
    if minqty == nil then minqty = 1 end
if minqty == nil then minqty = 1 end
    local rowTxt = '\r\n|-'
if expIcon == nil then expIcon = '' end
    rowTxt = rowTxt..'\r\n|style ="text-align: left;"|'..source
if level == nil then level = 'N/A' end
    rowTxt = rowTxt..'\r\n|style ="text-align: left;"|'..type
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 = {}


    rowTxt = rowTxt..'\r\n|style ="text-align: right;" data-sort-value:"'..qty..'"|'..minqty
--Alright, time to go through a few ways to get the item
    if qty ~= minqty then rowTxt = rowTxt..' - '..qty end
--First up: Can we kill somebody and take theirs?
    rowTxt = rowTxt..'\r\n|style ="text-align: right;"|'..Shared.round(chance, 2, 2)..'%'
for i, drop in ipairs(p._getItemMonsterSources(item)) do
    return rowTxt
local monster = GameData.getEntityByID('monsters', drop.id)
  end
local iconName = monster.name
  local dropRows = {}
if SourceOverrides[drop.id] ~= nil then
 
iconName = SourceOverrides[drop.id]
  --Alright, time to go through a few ways to get the item
end
  --First up: Can we kill somebody and take theirs?
  for i, monster in pairs(MonsterData.Monsters) do
    local minqty = 1
    local qty = 1
    local chance = 0
    local wt = 0
    local totalWt = 0
    --Only add bones if this monster has loot (ie appears outside a dungeon) and isn't a boss
    --... unless we're looking for Shards of course, at which point we'll take any monster with the right bones
    if ((monster.lootTable ~= nil and not monster.isBoss) or Shared.contains(item.name, 'Shard')) and monster.bones == item.id then
      qty = monster.boneQty ~= nil and monster.boneQty or 1
      minqty = qty
      chance = 100
    elseif monster.lootTable ~= nil then
      for j, loot in pairs(monster.lootTable) do
        totalWt = totalWt + loot[2]
        if loot[1] == item.id then
          wt = loot[2]
          qty = loot[3]
        end
      end
      if wt > 0 then
        local lootChance = monster.lootChance ~= nil and monster.lootChance or 100
        chance = ((wt * lootChance) / (totalWt * 100)) * 100
      end
    end
    if chance > 0 then
      --If we're dealing with a boss, this is a Dungeon row instead
      if monster.isBoss and not Shared.contains(item.name, 'Shard') then
        local dung = Areas.getMonsterAreas(i - 1)[1]
        local sourceTxt = Icons.Icon({dung.name, type="dungeon", notext=true})
        table.insert(dropRows, {source = sourceTxt, type = '[[Dungeon]]', minqty = minqty, qty = qty, chance = chance})
      else
        local sourceTxt = Icons.Icon({monster.name, type='monster'})
        table.insert(dropRows, {source = sourceTxt, type = '[[Monster]]', minqty = minqty, qty = qty, chance = chance})
      end
    end
  end


  --Special exception for the Fire Cape as a bonus dungeon drop
if monster ~= nil then
  if item.name == 'Fire Cape' then
local monsterLevel = Monsters._getMonsterCombatLevel(monster)
      local sourceTxt = Icons.Icon({"Volcanic Cave", type="dungeon", notext=true})
table.insert(dropRows, {
      table.insert(dropRows, {source = sourceTxt, type = '[[Dungeon]]', minqty = 1, qty = 1, chance = 100})
source = Icons.Icon({iconName, type='monster'}),
  elseif item.name == 'Infernal Cape' then
level = Icons.Icon({'Combat', 'Monsters', notext=true}) .. ' Level ' .. Num.formatnum(monsterLevel),
      local sourceTxt = Icons.Icon({"Infernal Stronghold", type="dungeon", notext=true})
levelNum = monsterLevel,
      table.insert(dropRows, {source = sourceTxt, type = '[[Dungeon]]', minqty = 1, qty = 1, chance = 100})
minqty = drop.minQty,  
  end
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


  --Next: Can we find it by rummaging around in another item?
-- Is the item dropped from any dungeon?
  for i, item2 in pairs(ItemData.Items) do
local dungeonEntities = {
    if item2.dropTable ~= nil then
['Dungeon'] = GameData.rawData.dungeons,
      local qty = 1
['The Abyss'] = GameData.rawData.abyssDepths
      local chance = 0
}
      local wt = 0
for entity, dungeons in pairs(dungeonEntities) do
      local totalWt = 0
local iconType = entity == 'Dungeon' and 'dungeon' or 'combatArea'
      for j, loot in pairs(item2.dropTable) do
for i, dungeon in ipairs(dungeons) do
        totalWt = totalWt + loot[2]
if (dungeon.oneTimeRewardID ~= nil and item.id == dungeon.oneTimeRewardID) or
        if loot[1] == item.id then
(type(dungeon.rewardItemIDs) == 'table' and Shared.contains(dungeon.rewardItemIDs, item.id)) then
          wt = loot[2]
table.insert(dropRows, {
          if item2.dropQty ~= nil then qty = item2.dropQty[j] end
source = Icons.Icon({dungeon.name, type=iconType}),
        end
level = '[['..entity..']]',
      end
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=iconType}) .. (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


      if wt > 0 then
for i, stronghold in ipairs(GameData.rawData.strongholds) do
        chance = (wt / totalWt) * 100
for tier, tierData in pairs(stronghold.tiers) do
        local sourceTxt = Icons.Icon({item2.name, type='item'})
if type(tierData.rewards) == 'table' and type(tierData.rewards.items) == 'table' then
        table.insert(dropRows, {source = sourceTxt, type = '[[Chest]]', minqty = 1, qty = qty, chance = chance})
for i, reward in ipairs(tierData.rewards.items) do
      end
if reward.id == item.id then
    end
table.insert(dropRows, {
  end
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


  --Finally, let's try just stealing it
-- Can we find it in an openable item?
  local thiefType = Icons.Icon({"Thieving", type='skill'})
for i, item2 in ipairs(GameData.rawData.items) do
  for i, npc in pairs(SkillData.Thieving) do
if item2.dropTable ~= nil then
    local qty = 1
local minQty, maxQty, wt, totalWt = 1, 1, 0, 0
    local chance = 0
for j, loot in ipairs(item2.dropTable) do
    local wt = 0
totalWt = totalWt + loot.weight
    local totalWt = 0
if loot.itemID == item.id then
    if npc.lootTable ~= nil then
wt = loot.weight
      for j, loot in pairs(npc.lootTable) do
minQty = loot.minQuantity
        totalWt = totalWt + loot[2]
maxQty = loot.maxQuantity
        if loot[1] == item.id then
end
          wt = loot[2]
end
        end
      end
      if wt > 0 then
        chance = (wt / totalWt) * 75
        local sourceTxt = Icons.Icon({npc.name, type='thieving'})
        table.insert(dropRows, {source = sourceTxt, type = thiefType, minqty = 1, qty = qty, chance = chance})
      end
    end
  end


  --Bonus overtime: Special Fishing table & mining gem table. Also Rags to Riches
if wt > 0 then
  if item.type == 'Gem' then
local sourceTxt = Icons.Icon({item2.name, type='item'})
    local mineType = Icons.Icon({'Mining', type='skill'})
table.insert(dropRows, {
    local thisGemChance = Items.GemTable[item.name].chance
source = sourceTxt,
    table.insert(dropRows, {source = '[[Mining#Gems|Gem]]', type = mineType, minqty = 1, qty = 1, chance = thisGemChance})
level = '[[Chest]]',
    local magicType = Icons.Icon({'Magic', type = 'skill'})
minqty = minQty,  
    table.insert(dropRows, {source = Icons.Icon({"Rags to Riches I", type="spell"}), type = magicType, minqty = 1, qty = 1, chance = thisGemChance})
qty = maxQty,  
    table.insert(dropRows, {source = Icons.Icon({"Rags to Riches II", type="spell"}), type = magicType, minqty = 1, qty = 1, chance = thisGemChance})
weight = wt,  
  end
totalWeight = totalWt,  
expIcon = Icons.getExpansionIcon(item2.id)})
end
end
end


  if item.fishingCatchWeight ~= nil then
-- Can it be obtained from Thieving?
    local fishSource = '[[Fishing#Special|Special]]'
local thiefItems = Skills.getThievingSourcesForItem(item.id)
    local fishType = Icons.Icon({'Fishing', type='skill'})
for i, thiefRow in ipairs(thiefItems) do
    local thisChance = (item.fishingCatchWeight / Items.specialFishWt) * 100
local sourceTxt = ''
    table.insert(dropRows, {source = fishSource, type = fishType, minqty = 1, qty = 1, chance = thisChance})
if thiefRow.npc == 'all' then
  end
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


  if item.type == 'Junk' then
-- Fishing: Junk & Specials
    local fishSource = '[[Fishing#Junk|Junk]]'
if Shared.contains(SkillData.Fishing.junkItemIDs, item.id) then
    local fishType = Icons.Icon({'Fishing', type='skill'})
local fishSource = '[[Fishing#Junk|Junk]]'
    local thisChance = 100 / Items.junkCount
local fishType = Icons.Icon({'Fishing', type='skill'})
    table.insert(dropRows, {source = fishSource, type = fishType, minqty = 1, qty = 1, chance = thisChance})
local fishTotWeight = Shared.tableCount(SkillData.Fishing.junkItemIDs)
  end
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


  --Make sure to return nothing if there are no drop sources 
-- Mining: Gems, and also Alt. Magic spells producing random gems
  if Shared.tableCount(dropRows) == 0 then return '' end
if Shared.contains({'Gem', 'Superior Gem'}, item.type) then
local gemKeys = { 'randomGems', 'randomSuperiorGems' }
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 'RandomSuperiorGem'
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


  table.sort(dropRows, function(a, b) return a.chance > b.chance end)
--Make sure to return nothing if there are no drop sources
  for i, data in pairs(dropRows) do
if Shared.tableIsEmpty(dropRows) then return '' end
    result = result..buildRow(data.source, data.type, data.minqty, data.qty, data.chance)
  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


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


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


  return p._getItemLootSourceTable(item)
return p._getItemLootSourceTable(item)
end
end


function p._getItemShopTable(item)
function p._getItemUpgradeTable(item)
  local result = '{| class="wikitable"\r\n|-\r\n!colspan="2"|[[Shop]] Purchase'
local resultPart = {}
  result = result..'\r\n|-\r\n!style="text-align:right;"|Cost\r\n|'
local upgrade = GameData.getEntityByProperty('itemUpgrades', 'upgradedItemID', item.id)
  local cost = {}
if upgrade ~= nil then
  local qty = '1'
local upgradeCost = Common.getCostString({
  if item.buysFor ~= nil then
["items"] = upgrade.itemCosts,
    if item.buysFor > 0 then table.insert(cost, Icons.GP(item.buysFor)) end
["currencies"] = upgrade.currencyCosts
  elseif item.slayerCost ~= nil then
})
    table.insert(cost, Icons.SC(item.slayerCost))
  elseif Items.GloveTable[item.name] ~= nil then
    table.insert(cost, Icons.GP(Items.GloveTable[item.name].cost))
    qty = ' +'..Shared.formatnum(Items.GloveTable[item.name].charges)..' Charges'
  end
  if item.buysForLeather ~= nil then
    table.insert(cost, Icons.Icon({'Leather', type='item', qty=item.buysForLeather}))
  end
  if item.buysForItems ~= nil then
    for i, row in pairs(item.buysForItems) do
      local mat = Items.getItemByID(row[1])
      table.insert(cost, Icons.Icon({mat.name, type='item', qty=row[2]}))
    end
  end
  if Shared.tableCount(cost) == 0 then
    --If no cost is set, return an empty string
    return ''
  else
    result = result..table.concat(cost, '<br/>')
  end


  --For right now, only have requirements on Skillcapes
table.insert(resultPart, '{| class="wikitable"\r\n|-\r\n!colspan="2"|[[Upgrading Items|Item Upgrade]]')
  result = result..'\r\n|-\r\n!style="text-align:right;"|Requirements\r\n|'
table.insert(resultPart, '\r\n|-\r\n!style="text-align:right;"|Materials\r\n|')
  if item.name == 'Cape of Completion' then
table.insert(resultPart, upgradeCost)
    result = result..'100% Completion Log'
table.insert(resultPart, '\r\n|}')
  elseif item.name == 'Max Skillcape' then
end
    result = result..'Level 99 in all [[Skills]]'
return table.concat(resultPart)
  elseif Shared.contains(item.name, 'Skillcape') then
    local skillName = Shared.splitString(item.name)[1]
    result = result..Icons._SkillReq(skillName, 99)
  else
    result = result..'None'
  end
 
  result = result..'\r\n|-\r\n!style="text-align:right;"|Quantity\r\n|'..qty
  result = result..'\r\n|}'
  return result
end
end


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


  return p._getItemShopTable(item)
return p._getItemUpgradeTable(item)
end
end


function p._getItemUpgradeTable(item)
function p._getSuperheatSmithRecipe(item)
  local result = ''
local smithRecipe = GameData.getEntityByProperty(SkillData.Smithing.recipes, 'productID', item.id)
  if item.itemsRequired ~= nil then
if smithRecipe ~= nil and smithRecipe.categoryID == 'melvorD:Bars' then
    --First, get details on all the required materials
return smithRecipe
    local upgradeFrom = {}
end
    local materials = {}
    for i, row in pairs(item.itemsRequired) do
      local mat = Items.getItemByID(row[1])
      --Check to see if the source item can trigger the upgrade
      if mat.canUpgrade or (mat.type == 'Armour' and mat.canUpgrade == nil) then
        table.insert(upgradeFrom, Icons.Icon({mat.name, type='item'}))
      end
      table.insert(materials, Icons.Icon({mat.name, type='item', qty=row[2]}))
    end
    if item.trimmedGPCost ~= nil then
      table.insert(materials, Icons.GP(item.trimmedGPCost))
    end
    result = '{| class="wikitable"\r\n|-\r\n!colspan="2"|[[Upgrading Items|Item Upgrade]]'
    --[[result = result..'\r\n|-\r\n!style="text-align:right;"|Upgrades From\r\n|'
    result = result..table.concat(upgradeFrom, '<br/>')--]]
    result = result..'\r\n|-\r\n!style="text-align:right;"|Materials\r\n|'
    result = result..table.concat(materials, '<br/>')
    result = result..'\r\n|}'
  end
  return result
end
end


function p.getItemUpgradeTable(frame)
function p._getItemSuperheatTable(item)
  local itemName = frame.args ~= nil and frame.args[1] or frame
--Manually build the Superheat Item table
  local item = Items.getItem(itemName)
-- Validate that the item can be superheated
  if item == nil then
local smithRecipe = p._getSuperheatSmithRecipe(item)
    return "ERROR: No item named "..itemName.." exists in the data module"
if smithRecipe == nil then
  end
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


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


function p._getItemSuperheatTable(item)
function p.getItemSuperheatTable(frame)
  --Manually build the Superheat Item table
local itemName = frame.args ~= nil and frame.args[1] or frame
  local oreString = ''
local item = Items.getItem(itemName)
  local coalString = ''
if item == nil then
  for i, mat in pairs(item.smithReq) do
return Shared.printError('No item named "' .. itemName .. '" exists in the data module')
    local thisMat = Items.getItemByID(mat.id)
end
    if thisMat.name == 'Coal Ore' then
 
      coalString = ', '..Icons.Icon({thisMat.name, type='item', notext='true', qty=mat.qty})
return p._getItemSuperheatTable(item)
    else
      if string.len(oreString) > 0 then oreString = oreString..', ' end
      oreString =  oreString..Icons.Icon({thisMat.name, type='item', notext='true', qty=mat.qty})
    end
  end
  --Set up the header
  local superheatTable = '{|class="wikitable"\r\n!colspan="2"|Spell'
  superheatTable = superheatTable..'!!'..Icons.Icon({'Smithing', type='skill', notext='true'})..' Level'
  superheatTable = superheatTable..'!!'..Icons.Icon({'Magic', type='skill', notext='true'})..' Level'
  superheatTable = superheatTable..'!!'..Icons.Icon({'Magic', type='skill', notext='true'})..' XP'
  superheatTable = superheatTable..'!!'..Icons.Icon({item.name, type='item', notext='true'})..' Bars'
  superheatTable = superheatTable..'!!Ore!!Runes'
  --Loop through all the variants
  local spellNames = {'Superheat I', 'Superheat II', 'Superheat III', 'Superheat IV'}
  for i, sName in pairs(spellNames) do
    local spell = Magic.getSpell(sName, 'AltMagic')
    local rowTxt = '\r\n|-\r\n|'..Icons.Icon({spell.name, type='spell', notext=true, size=50})
    rowTxt = rowTxt..'||[['..spell.name..']]||'..item.smithingLevel
    rowTxt = rowTxt..'||'..spell.magicLevelRequired..'||'..spell.magicXP..'||'..spell.convertToQty
    rowTxt = rowTxt..'||'..oreString
    if spell.ignoreCoal ~= nil and not spell.ignoreCoal then rowTxt = rowTxt..coalString end
    rowTxt = rowTxt..'||style="text-align:center"|'
    for i, req in pairs(spell.runesRequired) do
      local rune = Items.getItemByID(req.id)
      if i > 1 then rowTxt = rowTxt..', ' end
      rowTxt = rowTxt..Icons.Icon({rune.name, type='item', notext=true, qty=req.qty})
    end
    rowTxt = rowTxt.."<br/>'''OR'''<br/>"
    for i, req in pairs(spell.runesRequiredAlt) do
      local rune = Items.getItemByID(req.id)
      if i > 1 then rowTxt = rowTxt..', ' end
      rowTxt = rowTxt..Icons.Icon({rune.name, type='item', notext=true, qty=req.qty})
    end
    superheatTable = superheatTable..rowTxt
  end
  --Add the table end and add the table to the result string
  superheatTable = superheatTable..'\r\n|}'
  return superheatTable
end
end


function p.getItemSuperheatTable(frame)
function p._getTownshipTraderTable(item)
  local itemName = frame.args ~= nil and frame.args[1] or frame
for i, tsResource in ipairs(SkillData.Township.itemConversions.fromTownship) do
  local item = Items.getItem(itemName)
for j, tradeDef in ipairs(tsResource.items) do
  if item == nil then
if tradeDef.itemID == item.id then
    return "ERROR: No item named "..itemName.." exists in the data module"
-- Item found, build table
  end
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 p._getItemSuperheatTable(item)
return table.concat(resultPart)
end
end
end
return ''
end
end


function p._getItemSourceTables(item)
function p._getItemSourceTables(item)
  local result = ''
local resultPart = {}
  local shopTable = p._getItemShopTable(item)
local shopTable = Shop._getItemShopTable(item)
  if string.len(shopTable) > 0 then
if shopTable ~= '' then
    result = result..'===Shop===\r\n'..shopTable
table.insert(resultPart, '===Shop===\r\n'..shopTable)
  end
end


  local creationTable = p._getCreationTable(item)
local creationTable = p._getCreationTable(item)
  if string.len(creationTable) > 0 then  
if creationTable ~= '' then
    if string.len(result) > 0 then result = result..'\r\n' end
if not Shared.tableIsEmpty(resultPart) then table.insert(resultPart, '\r\n') end
    result = result..'===Creation===\r\n'..creationTable  
table.insert(resultPart, '===Creation===\r\n'..creationTable)
  end
end


  local upgradeTable = p._getItemUpgradeTable(item)
local upgradeTable = p._getItemUpgradeTable(item)
  if string.len(upgradeTable) > 0 then
if upgradeTable ~= '' then
    if string.len(result) > 0 then result = result..'\r\n' end
if not Shared.tableIsEmpty(resultPart) then table.insert(resultPart, '\r\n') end
    if string.len(creationTable) == 0 then result = result..'===Creation===\r\n' end
if creationTable ~= '' then table.insert(resultPart, '===Creation===\r\n') end
    result = result..upgradeTable
table.insert(resultPart, upgradeTable)
  end
end


  if item.type == 'Bar' then
local townshipTable = p._getTownshipTraderTable(item)
    result = result..'\r\n==='..Icons.Icon({'Alt Magic', type='skill'})..'===\r\n'..p._getItemSuperheatTable(item)
if townshipTable ~= '' then
  end
if not Shared.tableIsEmpty(resultPart) then table.insert(resultPart, '\n') end
table.insert(resultPart, '===Township===\n' .. townshipTable)
end


  local lootTable = p._getItemLootSourceTable(item)
if p._getSuperheatSmithRecipe(item) ~= nil then
  if string.len(lootTable) > 0 then
table.insert(resultPart, '\r\n==='..Icons.Icon({'Alt. Magic', type='skill'})..'===\r\n'..p._getItemSuperheatTable(item))
    if string.len(result) > 0 then result = result..'\r\n' end
end
    result = result..'===Loot===\r\n'..lootTable
 
  end
local lootTable = p._getItemLootSourceTable(item)
  return result
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
end


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


  return p._getItemSourceTables(item)
return p._getItemSourceTables(item)
end
end


function p.getCombatPassiveSlotItems(frame)
function p.getCombatPassiveSlotItems(frame)
  local table = "hello world"
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


  return table
--[[
-- 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)",
"Gold Topaz Ring",
"Astrology Lesser Relic",
"Mysterious Stone",
"Gold Bar",
"Raw Shrimp",
"Coal Ore",
"Rune Platebody",
"Arrow Shafts",
"Yew Longbow",
"Water Rune",
"Steam Rune",
"Controlled Heat Potion II",
"Wolf",
"Cyclops",
"Leprechaun",
"Redwood Logs",
"Carrot Cake",
"Carrot Cake (Perfect)",
"Mantalyme Herb",
"Carrot",
"Topaz",
"Rune Essence",
"Sanguine Blade",
"Ring of Power",
"Infernal Claw",
"Chapeau Noir",
"Stardust",
"Rope",
"Ancient Ring of Mastery",
"Mastery Token (Cooking)",
"Gem Gloves",
"Thief's Moneysack",
"Golden Stardust",
"Golden Star",
"Slayer Deterer",
"Paper",
"Lemon",
"Aranite Brush",
"Barrier Dust",
"Shadow Raven Nest",
"Void Wisp"
}
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
end
--]]


return p
return p

Latest revision as of 09:54, 6 September 2024

Documentation for this module may be created at Module:Items/SourceTables/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:Shop')
local Monsters = require('Module:Monsters')
local Skills = require('Module:Skills')
local Num = require('Module:Number')

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

function p._getCreationTable(item)
	local skill = ''
	local specialReq = nil
	local time = 0
	local maxTime = nil
	local lvl = 0
	local isAbyssal = false
	local xp = 0
	local qty = nil
	local req = nil

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

	local skillIDs = {
		['Gathering'] = {
			['Woodcutting'] = { recipeKey = 'trees' },
			['Fishing'] = { recipeKey = 'fish' },
			['Mining'] = { recipeKey = 'rockData' },
			['Farming'] = { recipeKey = 'recipes' }
		},
		['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 lvl, isAbyssal, xp, qty, req, time, maxTime = 0, false, 0, 0, nil, 0, nil
		for i, recipe in ipairs(skillData[dataProp.recipeKey]) do
			if recipe.productId == itemID then
				lvl, isAbyssal = Skills.getRecipeLevelRealm(localSkillID, recipe)
				xp = recipe.baseAbyssalExperience or recipe.baseExperience
				qty = recipe.baseQuantity or 1
				if localSkillID == 'Farming' then
					req = { recipe.seedCost }
					local category = GameData.getEntityByID(skillData.categories, recipe.categoryID)
					qty = 5 * category.harvestMultiplier
				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
				-- Special requirements
				if recipe.totalMasteryRequired ~= nil then
					specialReq = Icons.Icon({'Mastery', notext=true}) .. Num.formatnum(recipe.totalMasteryRequired) .. ' total [[' .. skill .. ']] [[Mastery]]'
				end
				table.insert(tables, p.buildCreationTable(skill, lvl, isAbyssal, xp, req, qty, time, maxTime, specialReq))
				-- Assumes item has a single source per skill
				break
			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 lvl, isAbyssal, xp, qty, req, time, maxTime = 0, false, 0, 0, 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
				-- 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
						specialReq = Icons._MasteryReq(item.name, levelUnlock.level, false)
					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', notext=true, 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', notext=true, qty=nonShardQty}))
						end
					end
					req = table.concat(shardCostArray, ', ')
					if not Shared.tableIsEmpty(otherCostArray) then
						local costLen = Shared.tableCount(otherCostArray)
						req = req .. '<br/>' .. (costLen == 1 and '' or 'and one of the following:<br/>') .. table.concat(otherCostArray, "<br/>'''OR''' ")
					end
					specialReq = 'At least 1 ' .. Icons.Icon({'Summoning%23Summoning Marks', item.name, img=item.name, type='mark'}) .. ' mark discovered'
					table.insert(tables, p.buildCreationTable(skill, lvl, isAbyssal, xp, req, qty, time, nil, specialReq))
				-- 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 reqSubPart = {}
						for k, itemCost in ipairs(altCost.itemCosts) do
							local reqItem = Items.getItemByID(itemCost.id)
							if reqItem == nil then
								table.insert(reqSubPart, itemCost.quantity .. 'x ?????')
							else
								table.insert(reqSubPart, Icons.Icon({reqItem.name, type='item', qty=itemCost.quantity}))
							end
						end

						table.insert(reqSubPart, Common.getCostString({ ["items"] = {}, ["currencies"] = recipe.currencyCosts}))
						table.insert(reqPart, table.concat(reqSubPart, ', '))
							table.insert(qtyPart, Num.formatnum(qty * altCost.quantityMultiplier))
					end
					local sep = "<br/>'''OR''' "
					req = table.concat(reqPart, sep)
					local qtyText = table.concat(qtyPart, sep)
					table.insert(tables, p.buildCreationTable(skill, lvl, isAbyssal, xp, req, qtyText, time, maxTime, specialReq))
				-- Finally, normal recipes with a single set of item costs
				elseif type(recipe.itemCosts) == 'table' and not Shared.tableIsEmpty(recipe.itemCosts) then
					table.insert(tables, p.buildCreationTable(skill, lvl, isAbyssal, xp, recipe.itemCosts, qty, time, maxTime, specialReq, 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 == item.id then
			table.insert(tables, p._buildAltMagicTable(altSpell))
		end
	end

	if Shared.tableIsEmpty(tables) then
		return ''
	else
		return table.concat(tables, '\r\n')
	end
end

function p.getAltMagicTable(frame)
	local spellName = frame.args ~= nil and frame.args[1] or frame
	local spell = Magic.getSpell(spellName, 'altMagic')
	if spell == nil then
		return Shared.printError('Could not find Alt. Magic spell "' .. spellName .. '"')
	else
		return p._buildAltMagicTable(spell)
	end
end

function p._buildAltMagicTable(spell)
	local resultPart = {}
	local imgType = Magic._getSpellIconType(spell)
	table.insert(resultPart, '{|class="wikitable"\r\n|-')
	table.insert(resultPart, '\r\n!colspan="2"|'..Icons.Icon({spell.name, type=imgType}))
	table.insert(resultPart, '\r\n|-\r\n!style="text-align:right;"|Requirements')
	table.insert(resultPart, '\r\n|'..Icons._SkillReq('Magic', spell.level))

	local costText = Magic._getAltSpellCostText(spell)
	if costText ~= nil then
		table.insert(resultPart, '\r\n|-\r\n!style="text-align:right;"|Materials')
		table.insert(resultPart, '\r\n| ' .. costText)
	end

	--Add runes
	table.insert(resultPart, '\r\n|-\r\n!style="text-align:right;"|Runes\r\n| ' .. Magic._getSpellRunes(spell))

	--Now just need the output quantity, xp, and casting time (which is always 2)
	table.insert(resultPart, '\r\n|-\r\n!style="text-align:right;"|Base Quantity\r\n|' .. spell.productionRatio)
	table.insert(resultPart, '\r\n|-\r\n!style="text-align:right;"|Base XP\r\n|' .. spell.baseExperience)
	table.insert(resultPart, '\r\n|-\r\n!style="text-align:right;"|Cast Time\r\n|2s')
	table.insert(resultPart, '\r\n|}')
	return table.concat(resultPart)
end

function p.buildCreationTable(skill, lvl, isAbyssal, xp, req, qty, time, maxTime, specialReq, currencyCost)
	if qty == nil then qty = 1 end
	local resultPart = {}
	table.insert(resultPart, '{|class="wikitable"')
	table.insert(resultPart, '\r\n!colspan="2"|Item ' .. (req == nil and 'Creation' or 'Production'))
	table.insert(resultPart, '\r\n|-\r\n!style="text-align: right;"|Requirements')
	table.insert(resultPart, '\r\n|'..Icons._SkillReq(skill, lvl, false, (isAbyssal and "melvorItA:Abyssal" or nil)))
	if specialReq ~= nil then table.insert(resultPart, '<br/>'..specialReq) end

	if req ~= nil then
		table.insert(resultPart, '\r\n|-\r\n!style="text-align: right;"|Materials\r\n|')
		if type(req) == 'table' then
			for i, mat in ipairs(req) do
				if i > 1 then table.insert(resultPart, '<br/>') end
				local matItem = Items.getItemByID(mat.id)
				if matItem == nil then
					table.insert(resultPart, mat.quantity..'x ?????')
				else
					table.insert(resultPart, Icons.Icon({matItem.name, type='item', qty=mat.quantity}))
				end
			end
			
			if currencyCost ~= nil then
				table.insert(resultPart, Common.getCostString({ ["items"] = {}, ["currencies"] = currencyCost }))
			end
		else
			table.insert(resultPart, req)
		end
	end
	table.insert(resultPart, '\r\n|-\r\n!style="text-align:right;"|Base Quantity')
	table.insert(resultPart, '\r\n|'..qty)
	table.insert(resultPart, '\r\n|-\r\n!style="text-align:right;"|Base Experience')
	table.insert(resultPart, '\r\n|'..Num.formatnum(xp)..' XP')
	table.insert(resultPart, '\r\n|-\r\n!style="text-align:right;"|Base Creation Time')
	table.insert(resultPart, '\r\n|'..Shared.timeString(time, true))
	if maxTime ~= nil and maxTime > time then table.insert(resultPart, ' - '..Shared.timeString(maxTime, true)) end
	table.insert(resultPart, '\r\n|}')

	return table.concat(resultPart)
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._getCreationTable(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
		local iconType = entity == 'Dungeon' and 'dungeon' or 'combatArea'
		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=iconType, 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=iconType, 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' },
			['Mining'] = { recipeKey = 'rockData' },
			['Farming'] = { recipeKey = 'recipes' }
		},
		['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
			if recipe.productId == item.id 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) 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
		local iconType = entity == 'Dungeon' and 'dungeon' or 'combatArea'
		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=iconType}), 
						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=iconType}) .. (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'}, item.type) then
		local gemKeys = { 'randomGems', 'randomSuperiorGems' }
		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 'RandomSuperiorGem'
				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

	--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._getItemUpgradeTable(item)
	local resultPart = {}
	local upgrade = GameData.getEntityByProperty('itemUpgrades', 'upgradedItemID', item.id)
	if upgrade ~= nil then
		local upgradeCost = Common.getCostString({
			["items"] = upgrade.itemCosts,
			["currencies"] = upgrade.currencyCosts
		})

		table.insert(resultPart, '{| class="wikitable"\r\n|-\r\n!colspan="2"|[[Upgrading Items|Item Upgrade]]')
		table.insert(resultPart, '\r\n|-\r\n!style="text-align:right;"|Materials\r\n|')
		table.insert(resultPart, upgradeCost)
		table.insert(resultPart, '\r\n|}')
	end
	return table.concat(resultPart)
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._getItemUpgradeTable(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

function p._getItemSourceTables(item)
	local resultPart = {}
	local shopTable = Shop._getItemShopTable(item)
	if shopTable ~= '' then
		table.insert(resultPart, '===Shop===\r\n'..shopTable)
	end

	local creationTable = p._getCreationTable(item)
	if creationTable ~= '' then
		if not Shared.tableIsEmpty(resultPart) then table.insert(resultPart, '\r\n') end
		table.insert(resultPart, '===Creation===\r\n'..creationTable)
	end

	local upgradeTable = p._getItemUpgradeTable(item)
	if upgradeTable ~= '' then
		if not Shared.tableIsEmpty(resultPart) then table.insert(resultPart, '\r\n') end
		if creationTable ~= '' then table.insert(resultPart, '===Creation===\r\n') end
		table.insert(resultPart, upgradeTable)
	end

	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)",
		"Gold Topaz Ring",
		"Astrology Lesser Relic",
		"Mysterious Stone",
		"Gold Bar",
		"Raw Shrimp",
		"Coal Ore",
		"Rune Platebody",
		"Arrow Shafts",
		"Yew Longbow",
		"Water Rune",
		"Steam Rune",
		"Controlled Heat Potion II",
		"Wolf",
		"Cyclops",
		"Leprechaun",
		"Redwood Logs",
		"Carrot Cake",
		"Carrot Cake (Perfect)",
		"Mantalyme Herb",
		"Carrot",
		"Topaz",
		"Rune Essence",
		"Sanguine Blade",
		"Ring of Power",
		"Infernal Claw",
		"Chapeau Noir",
		"Stardust",
		"Rope",
		"Ancient Ring of Mastery",
		"Mastery Token (Cooking)",
		"Gem Gloves",
		"Thief's Moneysack",
		"Golden Stardust",
		"Golden Star",
		"Slayer Deterer",
		"Paper",
		"Lemon",
		"Aranite Brush",
		"Barrier Dust",
		"Shadow Raven Nest",
		"Void Wisp"
	}
	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