Module:Uses material list

From the RuneScape Wiki, the wiki for all things RuneScape
Jump to navigation Jump to search
Module documentation
This documentation is transcluded from Module:Uses material list/doc. [edit] [history] [purge]
Module:Uses material list's function main is invoked by Template:Uses material list.
Module:Uses material list loads data from Module:Infobox Recipe/data.
Function list
L 26 — toolLink
L 31 — toolToAuxiliaryLink
L 45 — processToAuxiliaryLink
L 54 — facilityToAuxiliaryLink
L 68 — toAuxiliaryLink
L 84 — shouldHideAuxiliary
L 97 — cleanName
L 107 — cleanNameSort
L 114 — askBucket
L 138 — p.main
L 142 — stripDoseName
L 146 — findInRecipe
L 160 — roundToInt
L 168 — p._main
L 264 — editbtn
L 268 — itemLink
L 282 — itemDisplay
L 301 — itemImg
L 310 — printRow
L 542 — p.printTable

-- <pre>
require("strict")
local p = {}
local plink = require("Module:Plink")._plink
local geprice = require('Module:ExchangeLite').price
local commas = require('Module:Addcommas')._add
local skillpic = require('Module:Skill_clickpic')._main
local hc = require('Module:Paramtest').has_content
local yesno = require('Module:Yesno')
local purge = require('Module:Purge')._purge
local lang = mw.getContentLanguage()
local arr = require('Module:Array')

local TOOLBELT_TAG = '<sup>(<span class="hover-text" title="This item can be added to the tool belt. Some items are automatically added by default."}}>tb</span>)</sup>'
local toolsList, toolbeltTools, processIcons, facilitiesIcons, facilitiesList
local auxiliaryOrder = { "process", "tool", "facility" }
do
    local _data = mw.loadData("Module:Infobox Recipe/data")
    toolsList = _data.toolsList
    toolbeltTools = _data.toolbeltTools
    processIcons = _data.processIcons
    facilitiesIcons = _data.facilitiesIcons
    facilitiesList = _data.facilitiesList
end

local function toolLink(name, txt)
    local toolbelt_tag = arr.contains(toolbeltTools, name) and TOOLBELT_TAG or ''
    return (txt or plink(name)) .. toolbelt_tag
end

local function toolToAuxiliaryLink(item)
    local tools = toolsList[item]
    if type(tools) == "table" then
        local link = toolLink(item, tools[1])
        local altTool = tools.alt
        if altTool then
            link = link .. " /&nbsp;" .. toolLink(altTool)
        end
        return link
    else
        return toolLink(item, tools)
    end
end

local function processToAuxiliaryLink(item)
    local processIcon = processIcons[item]
    if processIcon ~= nil then
        return string.format("%s [[%s]]", processIcon, item)
    else
        return string.format("[[%s]]", item)
    end
end

local function facilityToAuxiliaryLink(item)
    local facilities = facilitiesList[item]
    if facilities ~= nil then
        return string.format("%s", facilities)
    end

    local facilitiesIcon = facilitiesIcons[item]
    if facilitiesIcon ~= nil then
        return string.format("%s [[%s]]", facilitiesIcon, item)
    else
        return string.format("[[%s]]", item)
    end
end

local function toAuxiliaryLink(kind, txt)
    local links = {}
    local items = mw.text.split(txt, "%s*,%s*")
    for _, item in ipairs(items) do
        if kind == "tool" then
            table.insert(links, toolToAuxiliaryLink(item))
        elseif kind == "process" then
            table.insert(links, processToAuxiliaryLink(item))
        elseif kind == "facility" then
            table.insert(links, facilityToAuxiliaryLink(item))
        end
    end

    return table.concat(links, ", ")
end

local function shouldHideAuxiliary(bucket_data)
    for _, bucket_item in ipairs(bucket_data) do
        for _, v in ipairs(auxiliaryOrder) do
            local auxiliary = bucket_item.production_json[v] or ""
            if auxiliary ~= "" then
                return false
            end
        end
    end

    return true
end

local function cleanName(name)
    local cleaned = string.gsub(name, "%p", "")

    if #cleaned then
        return cleaned
    end

    return name
end

local function cleanNameSort(t1, t2)
    local clean1 = cleanName(t1.page_name)
    local clean2 = cleanName(t2.page_name)

    return clean1 < clean2
end

local function askBucket(name, limit)
    local records = bucket("recipe")
        .select("page_name", "production_json", "Category:Removed content", "Category:Pages using information from game APIs or cache")
        .where('uses_material', name)
        .limit(limit)
        .orderBy('page_name', 'asc')
        .run()

    if records == nil then
        return nil
    end

    local return_records = {}
    for _, r in ipairs(records) do
        local json = mw.text.jsonDecode(r.production_json)
        if json.outputs then -- Filter out things like skilling methods that don't produce any output
            r.production_json = json
            table.insert(return_records, r)
        end
    end

    return return_records
end

function p.main(frame)
    return p._main(frame:getParent())
end

local function stripDoseName(name)
    return string.gsub(name, ' ?%(%d+%)$', '')
end

local function findInRecipe(tbl, target)
    if tbl == nil then
        return nil
    end

    for _, item in ipairs(tbl) do
        if item.page == target then
            return item
        end
    end

    return nil
end

local function roundToInt(num)
	if num >= 0 then
		return math.floor(num + 0.5)
	end
	
	return math.ceil(num - 0.5)
end

function p._main(frame)
    local args = frame.args
    local material = args[1] or args.material or mw.title.getCurrentTitle().text

    --mw.log('[Uses material list] looking for material: ' .. material)
    local bucket_records = askBucket(material, tonumber(args.limit) or 50)

    local verscount = 0
    if args.versions then
        local vernum = tonumber(args.versions)
        if vernum then
            verscount = math.floor(vernum)
        end
    end

    local res = {}
    if verscount > 0 then
        local variant_recipes = {}
        local prefix = stripDoseName(material)

        for vers = 1, verscount, 1 do
            local versname = prefix .. ' (' .. vers .. ')'
            --mw.log('[Uses material list] looking for material: ' .. versname)

            local ver_data = askBucket(versname, tonumber(args.limit) or 50)
            -- local verres = p.printTable(args, versname, bucket_data)

            variant_recipes[vers] = { portion = tostring(vers), name = versname, data = ver_data }
        end

        for _, r in ipairs(bucket_records or {}) do
            local item = findInRecipe(r.production_json.materials, prefix)

            if item ~= nil then
                local target_group = variant_recipes[item.portion or 1]
                if target_group == nil then
                    variant_recipes[item.portion or 1] = { portion = tostring(item.portion), name = item.name, data = { r } }
                else
                    table.insert(target_group.data, r)
                end
            end
        end

        local ordered_variants = {}
        for _, r in pairs(variant_recipes) do
            table.insert(ordered_variants, r)
        end

        table.sort(ordered_variants, function(a, b)
            return a.portion < b.portion
        end)

        local rescount = 0
        local emptyvers = {}
        for _, v in ipairs(ordered_variants) do
            local verres = p.printTable(args, v.name, v.data)
            if not verres then
                table.insert(emptyvers, v.name)
            else
                if args.headers and hc(args.headers) then
                    if string.lower(args.headers) == 'potion' or string.lower(args.headers) == 'potions' then
                        table.insert(res, string.format('===From %s doses===', v.portion))
                    else
                        table.insert(res, string.format('===' .. args.headers .. '===', v.name))
                    end
                else
                    table.insert(res, '===' .. v.name .. '===')
                end

                table.insert(res, verres)
                rescount = rescount + 1
            end
        end

        if rescount == 0 then
            return
            'Failed to find products with that material and versions - ensure it is spelled correctly and version count is correct. (ERR: no results from Bucket)[[Category:Empty drop lists]]'
        end

        if #emptyvers > 0 then
            table.insert(res, 1, '')
            local str = string.format(
                "The following versions have no products: %s. This, and the following lists are auto-generated (%s).",
                table.concat(emptyvers, ', '), purge('Products', 'click here to update', 'span'))
            table.insert(res, 1, str)
        end

        return table.concat(res, '\n')
    elseif not bucket_records or #bucket_records == 0 then
        return 'Failed to find products with that material - ensure it is spelled correctly. (ERR: no results from Bucket)[[Category:Empty drop lists]]'
    else
        return p.printTable(args, material, bucket_records) or
            'Failed to find products with that material - ensure it is spelled correctly. (ERR: no mat found in results)[[Category:Empty drop lists]]'
    end
end

local function editbtn(prodLink)
    return '[' .. tostring(mw.uri.fullUrl(prodLink, 'action=edit')) .. ' ' .. ('(edit)') .. ']'
end

local function itemLink(item)
    local prodLink = item.page
    local prodVariant = item.variant

    local namestr = ""
    if prodVariant then
        namestr = string.format('%s#%s', prodLink, prodVariant)
    else
        namestr = string.format('%s', prodLink)
    end

    return namestr
end

local function itemDisplay(item)
    local prodLink = item.page
    local prodVariant = item.variant
    local prodName = item.name

    local namestr = ""
    if prodVariant and prodName then
        namestr = string.format('[[%s#%s|%s]]', prodLink, prodVariant, prodName)
    elseif prodVariant then
        namestr = string.format('[[%s#%s|%s]]', prodLink, prodVariant, prodLink)
    elseif prodName then
        namestr = string.format('[[%s|%s]]', prodLink, prodName)
    else
        namestr = string.format('[[%s]]', prodLink)
    end

    return namestr
end

local function itemImg(item)
    local mat_img_src = item.image
    if mat_img_src ~= nil and mat_img_src ~= "no" then
        return '[[File:' .. mat_img_src .. '|link=' .. itemLink(item) .. '|30x30px|frameless]]'
    else
        return ''
    end
end

local function printRow(t, recipe, material, hideGE, hideAuxiliary, showprofit, historic, cacheOnly)
    local prow = t:tag('tr')

    local output = recipe.outputs[1]
    local namestr = itemDisplay(output)

    if namestr ~= '' then
        namestr = string.format('%s × %s', commas(output.quantity or 1), namestr)
    end

    if string.find(recipe.method or '', '%S') then
        namestr = namestr .. string.format('<br><small>(%s)</small>', recipe.method)
    end

    local improved = tonumber(recipe.improved) or 0
    if improved > 0 then
        namestr = namestr .. string.format('<br><small>(Improved +%s recipe)</small>', recipe.improved)
    end

    if historic then
        namestr = namestr .. '<span title="Previously used" style="cursor:help;"><small><b> †</b></small></span>'
        prow.addClass('products-hist')
    end
    if cacheOnly then
        namestr = namestr .. '<span title="From the game APIs or cache" style="cursor:help;"><small><b> ‡</b></small></span>'
    end

    local mats_table = mw.html.create('table')
    mats_table:addClass('products-materials')
        :attr{
            cellspacing = 0,
        }
        :css{
            margin = 0,
        }

    local mats_sort = 0
    local cost_raw = 0
    local found_material = false
    for _, mat_info in ipairs(recipe.materials) do
        local mat_tr = mats_table:tag('tr')

        local qty = string.gsub(mat_info.quantity, '%-', '–')

        local matpage = mat_info.page
        local matnm = mat_info.name
        local matnm_lc = matnm:lower()
        local mat_lc = material:lower()
        if matnm_lc == mat_lc or matpage:lower() == mat_lc then
            found_material = true
            mat_tr:addClass('production-selected')
            mats_sort = tonumber(qty) or 0
        end

        local product = string.format('%s × ', commas(qty))
        local matimg = itemImg(mat_info)

        mat_tr:tag('td')
            :css{
                ['text-align'] = 'right',
                ['white-space'] = 'pre',
            }
            :wikitext(product)
            :tag('td')
            :css{
                ['text-align'] = 'center',
            }
            :wikitext(matimg)
            :tag('td')
            :css{
                ['text-align'] = 'left',
                ['white-space'] = 'pre',
                ['width'] = '100%',
            }
            :wikitext('&nbsp;')
            :wikitext(itemDisplay(mat_info))
        local price = (matnm_lc == "coins" and 1) or geprice(matnm) or 0
        cost_raw = cost_raw + price * (qty or 1)
    end

    if found_material == false then
        return nil
    end

    local price_raw, price_total
    if not hideGE or showprofit then
        price_raw = geprice(output.name)
        if price_raw then
            price_total = roundToInt(price_raw * (output.quantity or 1))
        end
    end

    local price_cell
    if not hideGE then
        price_cell = mw.html.create('td')

        if price_raw then
            price_cell:wikitext(commas(price_total))
                :attr('data-sort-value', price_total)
        else
            price_cell:wikitext('N/A')
                :addClass('table-na')
                :attr{ ['title'] = 'This item has no available Grand Exchange price.', ['data-sort-value'] = 0 }
        end
    end

    local memberstr
    local members = yesno(recipe.members, nil)
    if members == true then
        memberstr = "[[File:P2P icon.png|30px|link=Members|alt=Members]]"
    elseif members == false then
        memberstr = "[[File:F2P icon.png|30px|link=Free-to-play|alt=Free-to-play]]"
    else
        memberstr = '<small>Unknown</small></br>' .. editbtn(output.name)
    end

    local skills_cell = mw.html.create('td')
    skills_cell:addClass('plainlist')

    local skills_ul = skills_cell:tag('ul')
    skills_ul:addClass('skills-list')

    local experience_cell = mw.html.create('td')
    experience_cell:addClass('plainlist')

    local experience_ul = experience_cell:tag('ul')
    experience_ul:addClass('skills-list')

    if #recipe.skills == 0 then
        local skill_li = skills_ul:tag('li')
        skill_li:wikitext(string.format('None'))
        skills_cell:addClass('table-na'):css('text-align', 'center')

        local experience_li = experience_ul:tag('li')
        experience_li:wikitext(string.format('None'))
        experience_cell:addClass('table-na'):css('text-align', 'center')
    else
        for index, v in ipairs(recipe.skills) do
            local skill_li = skills_ul:tag('li')
            skill_li
                :attr('data-sort-value', index == 1 and v.level or '')
            if v.level == '?' or v.level == 'Varies' then
                skill_li:wikitext(v.level .. ' ' .. skillpic(lang:ucfirst(v.name)))
            else
                skill_li:wikitext(skillpic(lang:ucfirst(v.name), v.level))
            end
            local experience_li = experience_ul:tag('li')
            local experience = tonumber(string.gsub(v.experience, ',', ''), 10)
            experience_li
                :attr('data-sort-value', index == 1 and v.experience or '')
                :wikitext(experience ~= nil and skillpic(lang:ucfirst(v.name), experience) or v.experience)
        end
    end

    local auxiliary_cell
    if not hideAuxiliary then
        -- Process/Facility/Tool. Not a great way to generalise this unless all of
        -- these, plus "skill used", are collected under something like "auxiliary"
        auxiliary_cell = mw.html.create('td')

        local auxiliaryList = {}
        for _, v in ipairs(auxiliaryOrder) do
            local auxiliaryLink = recipe[v] and toAuxiliaryLink(v, recipe[v]) or ""
            if auxiliaryLink ~= "" then
                table.insert(auxiliaryList, auxiliaryLink)
            end
        end

        if #auxiliaryList > 0 then
            auxiliary_cell:wikitext(table.concat(auxiliaryList, ", "))
        else
            auxiliary_cell:wikitext(string.format('N/A')):addClass('table-na'):css('text-align', 'center')
        end
    end

    prow
    -- Item (image)
        :tag('td')
        :addClass('inventory-image')
        :wikitext(itemImg(output))
        :done()
        :tag('td')
        :attr('data-sort-value', cleanName(output.name))
        :wikitext(namestr)
        :done()
    -- Members
        :tag('td')
        :wikitext(memberstr)
        :done()

    -- Process/Facility/Tool
    if not hideAuxiliary then
        prow:node(auxiliary_cell)
    end

    prow
    -- Skills
        :node(skills_cell)
    -- Experience
        :node(experience_cell)
    -- Materials
        :tag('td')
        :attr('data-sort-value', mats_sort)
        :node(mats_table)
        :done()

    -- Grand Exchange price
    if not hideGE then
        prow:node(price_cell)
    end

    -- Profit
    if showprofit then
        if price_raw then
            local profit1 = roundToInt(price_total - cost_raw)
            local profclass = profit1 >= 0 and 'text-green' or 'text-red'
            prow
                :tag('td')
                :wikitext(commas(profit1))
                :addClass(profclass)
                :attr('data-sort-value', profit1)
        else
            prow
                :tag('td')
                :wikitext('N/A')
                :addClass('table-na')
                :attr{ ['title'] = 'This item has no available GE price.', ['data-sort-value'] = 0 }
        end
    end
    return true
end

function p.printTable(args, material, bucket_data)
    local hideGE = yesno(args.hideGE or args.hidege) or false
    local showProfit = yesno(args.profit) or false

    local intro = string.format(
        "<div class='seealso prodlistintro'>For an exhaustive list of all products, see <span class='plainlinks'>[%s here]</span>.</div>",
        tostring(mw.uri.fullUrl('RuneScape:Autolists/full')) ..
        '#' .. mw.uri.buildQueryString{ type = 'products', page = material })

    table.sort(bucket_data, cleanNameSort)

    local hideAuxiliary = shouldHideAuxiliary(bucket_data)

    local columns = 0
    local t = mw.html.create('table'):addClass('wikitable sortable products-list sticky-header')
    if hc(args.class) then
        t:addClass(args.class)
    end

    local ttlrow = t:tag('tr')
        :tag('th')
        :attr('colspan', '2')
        :wikitext('Product')
        :done()
        :tag('th')
        :wikitext('Members')
        :done()
    t:addClass("align-center-" .. (columns + 1) .. " align-center-" .. (columns + 3))
    columns = columns + 3

    if (not hideAuxiliary) then
        ttlrow:tag('th')
            :wikitext('Using')
            :done()
        t:addClass("align-center-" .. (columns + 1))
        columns = columns + 1
    end

    ttlrow:tag('th')
        :wikitext('Skills')
        :done()
        :tag('th')
        :wikitext('Experience')
        :done()
        :tag('th')
        :wikitext('Materials')
        :done()
    t:addClass("align-right-" .. (columns + 1) .. " align-right-" .. (columns + 2))
    columns = columns + 3

    if (not hideGE) then
        ttlrow:tag('th')
            :wikitext('GE price')
            :done()
        t:addClass("align-right-" .. (columns + 1))
        columns = columns + 1
    end

    if showProfit then
        ttlrow:tag('th')
            :wikitext('Profit')
            :done()
        t:addClass("align-right-" .. (columns + 1))
        columns = columns + 1
    end

    local rows = 0
    local hasHistoric = false
    local hasCacheOnly = false
    for _, bucket_item in ipairs(bucket_data) do
        local currentHistoric = bucket_item["Category:Removed content"]
        local currentCacheOnly = bucket_item["Category:Pages using information from game APIs or cache"]
        hasHistoric = hasHistoric or currentHistoric
        hasCacheOnly = hasCacheOnly or currentCacheOnly
        local row = printRow(t, bucket_item.production_json, material, hideGE, hideAuxiliary, showProfit, currentHistoric, currentCacheOnly)
        if row ~= nil then
            rows = rows + 1
        end
    end

    if hasHistoric or hasCacheOnly then
        local captions = {}
        if hasHistoric then
            table.insert(captions, ('<small>† Denotes discontinued recipes that used [[%s]]</small>'):format(material))
        end
        if hasCacheOnly then
            table.insert(captions, ('<small>‡ Denotes recipes that use [[%s]] but were obtained from the game [[Application programming interface|APIs]] or [[Jagex cache|cache]] and may not be available</small>'):format(material))
        end
        t:tag('caption')
            :css('caption-side', 'bottom')
            :wikitext(table.concat(captions, '<br>'))
            :done()
    end

    if rows == 0 then
        return false
    end

    return intro .. '\n' .. tostring(t)
end

return p
--</pre>