Module:Item acquisition: Difference between revisions

From Path of Exile 2 Wiki
Jump to navigation Jump to search
>Techhead7890
(links to pages in headers. Considered linking List of skill gems rewarded from quests for vendor rewards, but couldn't find the right place to put it.)
No edit summary
 
(28 intermediate revisions by 5 users not shown)
Line 1: Line 1:
-- ----------------------------------------------------------------------------
-------------------------------------------------------------------------------
-- Imports
--  
-- ----------------------------------------------------------------------------
--                           Module:Item acquisition
--
-- This module implements Template:Item acquisition.
-------------------------------------------------------------------------------


local getArgs = require('Module:Arguments').getArgs
require('Module:No globals')
local m_game = require('Module:Game')
local m_util = require('Module:Util')
local m_util = require('Module:Util')
local m_cargo = require('Module:Cargo')
local m_cargo = require('Module:Cargo')
local m_item_util = require('Module:Item util')
local m_quest_reward = require('Module:Quest reward')._shared
local m_quest_reward = require('Module:Quest reward')._shared
local f_item_link = require('Module:Item link').item_link


-- ----------------------------------------------------------------------------
local m_game = mw.loadData('Module:Game')
-- Strings
 
-- ----------------------------------------------------------------------------
-- Should we use the sandbox version of our submodules?
-- This section contains strings used by this module.
local use_sandbox = m_util.misc.maybe_sandbox('Item acquisition')
-- Add new strings here instead of in-code directly, this will help other
-- people to correct spelling mistakes easier and help with translation to
-- other PoE wikis.
--
-- TODO: Maybe move this out to a separate sub-page module
local i18n = {
    acquisition = {
        header = 'Item acquisition',
        link = '[[File:Questionmark.png|right|24px|link=Path_of_Exile_Wiki:How_to_edit_item_acquisition]]',
       
        drop_leagues = 'This item is restricted to areas with the league flag for %s active. There are additional ways to acquire league-restricted items, see [[Drop-restricted item#League-specific items|League-specific items]] for more details.',
        drop_disabled = 'This item is [[drop disabled]].',
        area_header = 'Area restrictions',
        area_legacy_header = 'Legacy area restrictions',
        area = 'This item can be acquired in the following areas:',
        monster = 'This item can be acquired from the following monsters:',
        monster_header = 'Monster restrictions',
       
        upgraded_from_header = 'Upgrade paths',
        upgraded_from = 'This item can be acquired through the following upgrade paths or [[vendor recipes]]:',
       
        ingredient_header = 'Usage in upgrade paths',
        ingredient = 'This item is used by upgrade paths or [[vendor recipes]] to create the following items:',
    },
   
    upgraded_from_table = {
        outcome = 'Outcome',
        ingredient = 'Ingredient',
        ingredient_amount = 'Amount',
        ingredient_notes = 'Ingredient<br>Notes',
        recipe_notes = 'General<br>Notes',
        type = 'Type',
        automatic = m_util.html.abbr('Automatic', 'Automatically added by the item template based on the item\'s attributes.'),
        manual = m_util.html.abbr('Manual', 'Manually added to the item\'s wiki page.'),
        old_data = m_util.html.abbr('Old data', 'Old data - please null-edit the item\'s wiki page.'),
    },
   
    quest_reward = {
        quest_rewards_header = 'Quest reward',
        quest_rewards_intro = 'This item is given as a [[quest reward]] for the following quests:',
        vendor_rewards_header = 'Vendor reward',
        vendor_rewards_intro = 'This item can be bought at the listed NPC [[vendor]]s, after completing the following quests:',
    },
}


-- ----------------------------------------------------------------------------
-- Lazy loading
-- Globals
local f_item_link -- require('Module:Item link').item_link
-- ----------------------------------------------------------------------------


local c = {}
-- The cfg table contains all localisable strings and configuration, to make it
-- easier to port this module to another wiki.
local cfg = use_sandbox and mw.loadData('Module:Item acquisition/config/sandbox') or mw.loadData('Module:Item acquisition/config')


c.MAX_ITEMS = 100
local i18n = cfg.i18n
c.COLLAPSE_TABLE = 30


-- ----------------------------------------------------------------------------
-- ----------------------------------------------------------------------------
Line 73: Line 31:


local h = {}
local h = {}
-- Lazy loading for Module:Item link
function h.item_link(args)
    if not f_item_link then
        f_item_link = require('Module:Item link').item_link
    end
    return f_item_link(args)
end
function h.head(str, level)
function h.head(str, level)
     local head = mw.html.create('h' .. (level or 3))
     local head = mw.html.create('h' .. (level or 3))
Line 79: Line 46:
end
end


function h.fetch_upgraded_from_sets(where_set, where_group)
function h.fetch_recipes(where_set, where_group)
     if where_set == "" or where_group == "" then
     if where_set == "" or where_group == "" then
         return {}, {}
         return {}, {}
Line 86: Line 53:
     local sets = {}
     local sets = {}
     local results = m_cargo.query(
     local results = m_cargo.query(
         {'upgraded_from_sets'},
         {'acquisition_recipes'},
         {
         {
             'upgraded_from_sets._pageName',
             'acquisition_recipes._pageName',
             'upgraded_from_sets.set_id',
             'acquisition_recipes.recipe_id',
             'upgraded_from_sets.text',
             'acquisition_recipes.result_amount',
             'upgraded_from_sets.automatic',
             'acquisition_recipes.description',
            'acquisition_recipes.automatic',
         },
         },
         {
         {
Line 100: Line 68:
     for _, row in ipairs(results) do
     for _, row in ipairs(results) do
         row.groups = {}
         row.groups = {}
         sets[row['upgraded_from_sets._pageName'] .. tonumber(row['upgraded_from_sets.set_id'])] = row
         sets[row['acquisition_recipes._pageName'] .. tonumber(row['acquisition_recipes.recipe_id'])] = row
     end
     end
      
      
     results = m_cargo.query(
     results = m_cargo.query(
         {'upgraded_from_groups'},
         {'acquisition_recipe_parts'},
         {
         {
             'upgraded_from_groups._pageName',
             'acquisition_recipe_parts._pageName',
             'upgraded_from_groups.set_id',
             'acquisition_recipe_parts.recipe_id',
             'upgraded_from_groups.group_id',
             'acquisition_recipe_parts.part_id',
             'upgraded_from_groups.notes',
             'acquisition_recipe_parts.notes',
             'upgraded_from_groups.amount',
             'acquisition_recipe_parts.amount',
             'upgraded_from_groups.item_name',
             'acquisition_recipe_parts.item_name',
             'upgraded_from_groups.item_page',
             'acquisition_recipe_parts.item_page',
         },
         },
         {
         {
Line 120: Line 88:
      
      
     for _, row in ipairs(results) do
     for _, row in ipairs(results) do
         sets[row['upgraded_from_groups._pageName'] .. tonumber(row['upgraded_from_groups.set_id'])].groups[tonumber(row['upgraded_from_groups.group_id'])] = row
         sets[row['acquisition_recipe_parts._pageName'] .. tonumber(row['acquisition_recipe_parts.recipe_id'])].groups[tonumber(row['acquisition_recipe_parts.part_id'])] = row
     end
     end
      
      
Line 129: Line 97:
      
      
     table.sort(sets_sort, function (a, b)
     table.sort(sets_sort, function (a, b)
         return tonumber(sets[a]['upgraded_from_sets.set_id']) < tonumber(sets[b]['upgraded_from_sets.set_id'])
         return tonumber(sets[a]['acquisition_recipes.recipe_id']) < tonumber(sets[b]['acquisition_recipes.recipe_id'])
     end)
     end)
      
      
Line 135: Line 103:
end
end


function h.upgraded_from_table(tpl_args, sets, set_order, data_type, out, item_pages)
function h.recipe_table(tpl_args, item, sets, set_order, data_type, out, item_pages)
      
      
     -- Count the number of item pages:
     -- Count the number of item pages:
Line 156: Line 124:
         end
         end
         local set = sets[set_key]
         local set = sets[set_key]
         if set['upgraded_from_sets.text'] ~= nil then
         if set['acquisition_recipes.description'] ~= nil then
             set_notes = true
             set_notes = true
         end
         end
Line 162: Line 130:
         if #set.groups > 0 then
         if #set.groups > 0 then
             for i, group in ipairs(set.groups) do
             for i, group in ipairs(set.groups) do
                 if group['upgraded_from_groups.notes'] ~= nil then
                 if group['acquisition_recipe_parts.notes'] ~= nil then
                     group_notes = true
                     group_notes = true
                     break
                     break
Line 173: Line 141:
      
      
     -- sorting will mess up the table because of the rowspawns
     -- sorting will mess up the table because of the rowspawns
     local table_class = 'wikitable mw-collapsible mw-expanded'
     local table_class = 'wikitable responsive-table'
     if item_pages_count > c.COLLAPSE_TABLE then
     --[[if item_pages_count > cfg.COLLAPSE_TABLE then
         table_class = 'wikitable mw-collapsible mw-collapsed'
         table_class = 'wikitable mw-collapsible mw-collapsed'
     end
     end--]]
     tbl:attr('class', table_class)
     tbl:attr('class', table_class)
      
      
Line 186: Line 154:
         tr
         tr
             :tag('th')
             :tag('th')
                 :wikitext(i18n.upgraded_from_table.outcome)
                 :wikitext(i18n.recipe_table.result)
     end
     end
      
      
     tr
     tr
         :tag('th')
         :tag('th')
             :wikitext(i18n.upgraded_from_table.ingredient_amount)
             :wikitext(i18n.recipe_table.part_amount)
             :done()
             :done()
         :tag('th')
         :tag('th')
             :wikitext(i18n.upgraded_from_table.ingredient)
             :wikitext(i18n.recipe_table.part)
             :done()
             :done()
     if group_notes then
     if group_notes then
         tr
         tr
             :tag('th')
             :tag('th')
                 :wikitext(i18n.upgraded_from_table.ingredient_notes)
                 :wikitext(i18n.recipe_table.part_notes)
                 :done()
                 :done()
     end
     end
Line 205: Line 173:
         tr
         tr
             :tag('th')
             :tag('th')
                 :wikitext(i18n.upgraded_from_table.recipe_notes)
                 :wikitext(i18n.recipe_table.description)
                 :done()
                 :done()
     end
     end
     tr
     tr
         :tag('th')
         :tag('th')
             :wikitext(i18n.upgraded_from_table.type)
             :wikitext(i18n.recipe_table.meta)
             :done()
             :done()
     --
     --
Line 225: Line 193:
             if data_type == 'ingredient' then
             if data_type == 'ingredient' then
                 local str
                 local str
                 if tpl_args.no_link == nil and item_pages_count < c.MAX_ITEMS then
                 if tpl_args.no_link == nil and item_pages_count < cfg.MAX_ITEMS then
                     item_data = item_pages[set['upgraded_from_sets._pageName']][1]
                     local item_data = item_pages[set['acquisition_recipes._pageName']][1]
                     str = f_item_link{page=set['upgraded_from_sets._pageName'], name=item_data['items.name'], inventory_icon=item_data['items.inventory_icon'] or '', html=item_data['items.html'] or '', skip_query=true}
                     str = h.item_link{page=set['acquisition_recipes._pageName'], name=item_data['items.name'], inventory_icon=item_data['items.inventory_icon'] or '', html=item_data['items.html'] or '', skip_query=true}
                 else
                 else
                     str = string.format('[[%s]]', set['upgraded_from_sets._pageName'])
                     str = string.format('[[%s]]', set['acquisition_recipes._pageName'])
                 end
                 end
                  
                  
Line 239: Line 207:
             end
             end
              
              
            local tr2
             for i, group in ipairs(set.groups) do
             for i, group in ipairs(set.groups) do
                 if i <= 1 then
                 if i <= 1 then
Line 246: Line 215:
                 end
                 end
                 local str
                 local str
                 if tpl_args.no_link == nil and item_pages_count < c.MAX_ITEMS then
                 if tpl_args.no_link == nil and item_pages_count < cfg.MAX_ITEMS then
                     item_data = item_pages[group['upgraded_from_groups.item_page']][1]
                     local item_data = item_pages[group['acquisition_recipe_parts.item_page']][1]
                     str = f_item_link{
                     str = h.item_link{
                         page=group['upgraded_from_groups.item_page'],  
                         page=group['acquisition_recipe_parts.item_page'],  
                         name=item_data['items.name'],  
                         name=item_data['items.name'],  
                         inventory_icon=item_data['items.inventory_icon'] or '',  
                         inventory_icon=item_data['items.inventory_icon'] or '',  
Line 256: Line 225:
                     }
                     }
                 else
                 else
                     str = string.format('[[%s]]', group['upgraded_from_groups.item_page'])
                     str = string.format('[[%s]]', group['acquisition_recipe_parts.item_page'])
                 end
                 end
                  
                  
Line 262: Line 231:
                     :tag('td')
                     :tag('td')
                         :attr('data-sort-type', 'number')
                         :attr('data-sort-type', 'number')
                         :wikitext(group['upgraded_from_groups.amount'])
                         :wikitext(group['acquisition_recipe_parts.amount'])
                         :done()
                         :done()
                     :tag('td')
                     :tag('td')
Line 269: Line 238:
                  
                  
                 if group_notes then
                 if group_notes then
                     if group['upgraded_from_groups.notes'] then
                     if group['acquisition_recipe_parts.notes'] then
                         tr2
                         tr2
                             :tag('td')
                             :tag('td')
                                 :wikitext(group['upgraded_from_groups.notes'])
                                 :wikitext(group['acquisition_recipe_parts.notes'])
                     else
                     else
                         tr2:node(m_util.html.td.na{as_tag=true})
                         tr2
                            :node(
                                m_util.html.table_cell('na')
                            )
                     end
                     end
                 end
                 end
Line 280: Line 252:
              
              
             if set_notes then
             if set_notes then
                 if set['upgraded_from_sets.text'] then
                 if set['acquisition_recipes.description'] then
                     tr
                     tr
                         :tag('td')
                         :tag('td')
                             :attr('rowspan', #set.groups)
                             :attr('rowspan', #set.groups)
                             :wikitext(set['upgraded_from_sets.text'])
                             :wikitext(set['acquisition_recipes.description'])
                 else
                 else
                     tr
                     tr
                         :tag('td')
                         :node(
                             :attr('class', 'table-na')
                             m_util.html.table_cell('na')
                            :attr('rowspan', #set.groups)
                                :attr('rowspan', #set.groups)
                            :wikitext('N/A')
                        )
                            :done()
                 end
                 end
             end
             end
              
              
             local t
             local t
             if set['upgraded_from_sets.automatic'] == nil then
             if set['acquisition_recipes.automatic'] == '1' then
                t = i18n.upgraded_from_table.old_data
                 t = i18n.recipe_table.automatic
            elseif set['upgraded_from_sets.automatic'] == '1' then
                 t = i18n.upgraded_from_table.automatic
             else
             else
                 t = i18n.upgraded_from_table.manual
                 t = i18n.recipe_table.manual
             end
             end
              
              
Line 317: Line 286:
      
      
     if data_type == 'ingredient' then
     if data_type == 'ingredient' then
         out[#out+1] = h.head(i18n.acquisition.ingredient_header)
         out[#out+1] = h.head(i18n.acquisition.recipe_usage_heading)
         out[#out+1] = i18n.acquisition.ingredient
         out[#out+1] = string.format(i18n.acquisition.recipe_usage_text, item.name)
     else
     else
         out[#out+1] = h.head(i18n.acquisition.upgraded_from_header)
         out[#out+1] = h.head(i18n.acquisition.recipes_heading)
         out[#out+1] = i18n.acquisition.upgraded_from
         out[#out+1] = string.format(i18n.acquisition.recipes_text, item.name)
     end
     end
     out[#out+1] = '<br>'
     out[#out+1] = '<br>'
Line 338: Line 307:
         if row[rtbl .. '.classes'] then
         if row[rtbl .. '.classes'] then
             classes = {}
             classes = {}
             for _, class in ipairs(m_util.string.split(row[rtbl .. '.classes'], '')) do
             for _, class in ipairs(m_util.string.split(row[rtbl .. '.classes'], ',')) do
                 classes[class] = true
                 classes[class] = true
             end
             end
Line 345: Line 314:
         local tr = tbl:tag('tr')
         local tr = tbl:tag('tr')
         m_quest_reward.reward_tbl_row_head(tr, rtbl, row)
         m_quest_reward.reward_tbl_row_head(tr, rtbl, row)
               
 
        local cell = {
            [0] = {
                value = '✗',
                sort = 0,
                class = 'table-cell-xmark',
            },
            [1] = {
                value = '✓',
                sort = 1,
                class = 'table-cell-checkmark',
            },
        }
        if rtbl == 'quest_rewards' then
            local value = m_quest_reward.reward_tbl_extra_info(row)
           
            -- If there isn't any extra information the checkmark will do
            if value ~= '' then
                cell[1].value = value
                cell[1].class = nil
                cell[1].css = 'text-align: center;'
            end
        end
       
         if classes then
         if classes then
             for _, class in ipairs(m_game.constants.characters_order) do
             for _, class in ipairs(m_game.constants.characters_order) do
                local cell_value
                 if classes[m_game.constants.characters[class].name] then
                 if classes[m_game.constants.characters[class].name] then
                     cell_value = cell[1]
                     tr
                        :node(
                            m_util.html.table_cell('yes')
                        )
                 else
                 else
                     cell_value = cell[0]
                     tr
                        :node(
                            m_util.html.table_cell('no')
                        )
                 end
                 end
                tr:tag('td')
                    :attr('class', cell_value.class or '')
                    :attr('style', cell_value.css or '')
                    :attr('table-sort-value', cell_value.sort)
                    :wikitext(cell_value.value)
             end
             end
         else
         else
             tr:tag('td')
             tr
                 :attr('colspan', 7)
                 :node(
                :attr('class', cell[1].class or '')
                    m_util.html.table_cell('yes')
                :attr('style', cell[1].css or '')
                        :attr('colspan', 7)
                :attr('table-sort-value', cell[1].sort)
                 )
                 :wikitext(cell[1].value)
         end
         end
     end
     end
Line 397: Line 342:


-- ----------------------------------------------------------------------------
-- ----------------------------------------------------------------------------
-- Templates
-- Main functions
-- ----------------------------------------------------------------------------
-- ----------------------------------------------------------------------------


local p = {}
local function _main(tpl_args)
 
--
-- Template: Item acquisition
--
function p.item_acquisition (frame)
     --[[
     --[[
     Duplicates the information from the infobox in a more readable  
     Duplicates the information from the infobox in a more readable  
Line 415: Line 355:
     ]]
     ]]


    -- Get args
    local tpl_args = getArgs(frame, {
        parentFirst = true
    })
    frame = m_util.misc.get_frame(frame)
   
    tpl_args.page = tpl_args.page or tostring(mw.title.getCurrentTitle())
   
     local out = {}
     local out = {}
    local results
    local query
   
     out[#out+1] = tpl_args.acquisition_insert
     out[#out+1] = tpl_args.acquisition_insert
      
      
     -- fetch general item drop information that is used in multiple places in a
     -- fetch general item drop information that is used in multiple places in a
     -- single query to reduce performance hit
     -- single query to reduce performance hit
     local item_data = m_cargo.query(
     local tables = {'items'}
         {'items'},
    local fields = {
         {
        'items._pageID=_pageID',
            'items.drop_text',
        'items._pageName=_pageName',
            'items.drop_leagues',
        'items.name=name',
            'items.drop_areas__full',
        'items.class_id=class_id',
            'items.drop_monsters',
         'items.rarity_id=rarity_id',
            'items.drop_enabled',
         'items.base_item_page=base_item_page',
         },
        'items.drop_text=drop_text',
         {
        'items.acquisition_tags=acquisition_tags',
            where=string.format('items._pageName="%s"', tpl_args.page),
        'items.drop_areas=drop_areas',
             limit=1,
        'items.drop_monsters=drop_monsters',
         }
        'items.drop_enabled=drop_enabled',
     )
         'items.is_drop_restricted=is_drop_restricted',
    }
    local query = {
        limit = 1,
    }
    if tpl_args.page then
        -- Join with _pageData in order to check for page redirect
         tables[#tables+1] = '_pageData'
        query.join = 'items._pageName = _pageData._pageNameOrRedirect'
        query.where = string.format(
            '_pageData._pageName="%s"',
            tpl_args.page
        )
    else
        query.where = string.format(
             'items._pageName="%s"',
            tostring(mw.title.getCurrentTitle())
         )
    end
     local item_data = m_cargo.query(tables, fields, query)
     if #item_data > 0 then
     if #item_data > 0 then
         item_data = item_data[1]
         item_data = item_data[1]
    end
    if item_data == nil then
        error("Item search unexpectedly returned no results")
    elseif item_data.name == nil then
        error("Item search result unexpectedly has no name")
     end
     end
      
      
     -- ------------------------------------------------------------------------
     -- ------------------------------------------------------------------------
     -- Drop disabled item
     -- General drop restrictions
     -- ------------------------------------------------------------------------
     -- ------------------------------------------------------------------------
     local drop_enabled = m_util.cast.boolean(item_data['items.drop_enabled'])
     local drop_enabled = m_util.cast.boolean(item_data.drop_enabled)
    local drop_restricted = m_util.cast.boolean(item_data.is_drop_restricted)
    local is_unique = item_data.rarity_id == 'unique'
     if not drop_enabled then
     if not drop_enabled then
         out[#out+1] = i18n.acquisition.drop_disabled
         out[#out+1] = string.format(i18n.acquisition.drop_disabled, item_data.name)
         out[#out+1] = ' '
    elseif drop_restricted then
     end  
        -- Acquisition unkown
         if not item_data.drop_areas and not item_data.drop_monsters and not item_data.drop_text then
            -- Divination cards
            if item_data.class_id == 'DivinationCard' then
                out[#out+1] = mw.getCurrentFrame():expandTemplate{title = i18n.templates.divination_card_acquisition_unknown}
                out[#out+1] = '\n\n'
            end
        end
 
        out[#out+1] = string.format(i18n.acquisition.drop_restricted, item_data.name)
        if is_unique then
            out[#out+1] = ' '
            out[#out+1] = i18n.acquisition.cannot_be_chanced
        end
    else
        if not item_data.drop_areas then
            out[#out+1] = string.format(i18n.acquisition.drop_anywhere, item_data.name)
        end
        if is_unique then
            local base_item_data = m_cargo.query(
                {'items'},
                {
                    'items.is_corrupted=is_corrupted',
                    'items.drop_enabled=drop_enabled',
                },
                {
                    where = string.format('items._pageName="%s"', item_data.base_item_page),
                    groupBy = 'items._pageID',
                }
            )
            if #base_item_data > 0 then
                base_item_data = base_item_data[1]
                if m_util.cast.boolean(base_item_data.drop_enabled) and not m_util.cast.boolean(base_item_data.is_corrupted) then
                    out[#out+1] = ' '
                    out[#out+1] = i18n.acquisition.can_be_chanced
                end
            end
        end
     end
      
      
     -- ------------------------------------------------------------------------
     -- ------------------------------------------------------------------------
     -- Drop restrictions by league
     -- League-specific
     -- ------------------------------------------------------------------------
     -- ------------------------------------------------------------------------
     if item_data['items.drop_leagues'] and drop_enabled then
    local acquisition_tags = {}
         out[#out+1] = string.format(i18n.acquisition.drop_leagues, item_data['items.drop_leagues'])
     if item_data.acquisition_tags then
        out[#out+1] = ' '
        acquisition_tags = m_util.cast.table(item_data.acquisition_tags)
    end
    if m_util.table.contains(acquisition_tags, 'league-specific') and drop_enabled and is_unique then
        out[#out+1] = '\n\n'
         out[#out+1] = string.format(i18n.acquisition.league_specific_unique, item_data.name)
     end
     end
      
      
Line 470: Line 466:
     -- Drop restrictions by text
     -- Drop restrictions by text
     -- ------------------------------------------------------------------------
     -- ------------------------------------------------------------------------
     if item_data['items.drop_text'] and drop_enabled then
     if item_data.drop_text and drop_enabled then
         out[#out+1] = item_data['items.drop_text']
        out[#out+1] = '\n\n'
         out[#out+1] = item_data.drop_text
     end
     end
      
      
Line 477: Line 474:
     -- Drop restrictions by area
     -- Drop restrictions by area
     -- ------------------------------------------------------------------------
     -- ------------------------------------------------------------------------
   
     local drop_areas = {}
     local area_ids
     if item_data.drop_areas then
     if item_data['items.drop_areas__full'] then
         drop_areas = m_util.cast.table(item_data.drop_areas)
         area_ids = m_util.string.split(item_data['items.drop_areas__full'], ',')
    else
        area_ids = {}
     end
     end
   
     if #drop_areas > 0 then
    -- Handle legacy areas:
         local results = m_cargo.query(
    local legacy_area_ids = {
            {'areas'},
        -- 'MapWar%',
            {
        'MapAtlas%',
                'areas._pageName=_pageName',
        'Map2%',
                'areas.id=id',
        'MapTier%',
                'areas.name=name',
    }
                'areas.main_page=main_page',
    local condition_current = {}
            },
    local condition_legacy = {}
            {
    local order_legacy = {}
                where = string.format('areas.id IN ("%s") AND NOT areas.is_legacy_map_area', table.concat(drop_areas, '","')),
    for i,v in ipairs(legacy_area_ids) do
                orderBy = 'areas.name ASC',
        condition_current[#condition_current+1] = string.format('areas.id NOT LIKE "%s"', v)
                groupBy = 'areas._pageID',
        condition_legacy[#condition_legacy+1] = string.format('areas.id LIKE "%s"', v)
            }
        order_legacy[#order_legacy+1] = string.format('WHEN areas.id LIKE "%s" THEN %d', v,i)       
        )
    end
        if #results > 0 then
   
            local ul = mw.html.create('ul')
    local query_sets = {
            for _, row in ipairs(results) do
        {
                ul:tag('li')
            header=i18n.acquisition.area_header,
                    :wikitext(m_util.html.wikilink(row.main_page or row._pageName, row.name))
            condition=string.format('%s', table.concat(condition_current, ' AND ')),
            order='areas.name ASC',
        },
        {
            header=i18n.acquisition.area_legacy_header,
            condition=string.format('%s', table.concat(condition_legacy, ' OR ')),
            order=string.format([[
                CASE
                    %s
                    ELSE %d
                END ASC,
                areas.name ASC
            ]],
            table.concat(order_legacy, ' '),
            #order_legacy+1
            ),
        },
    }
       
     if #area_ids > 0 then
         for _, query_set in ipairs(query_sets) do
            local results = m_cargo.query(
                {'areas'},
                {
                    'areas._pageName',
                    'areas.id',
                    'areas.name',
                    'areas.main_page',
                },
                {
                    where=string.format('(%s) AND areas.id IN ("%s")', query_set.condition, table.concat(area_ids, '","')),
                    orderBy=query_set.order,
                    groupBy='areas.id',
                }
            )
            if #results > 0 then
                local ul = mw.html.create('ul')
                for _, row in ipairs(results) do
                    ul:tag('li')
                        :wikitext(string.format('[[%s|%s]]', row['areas.main_page'] or row['areas._pageName'], row['areas.name']))
                end           
                out[#out+1] = h.head(query_set.header)
                out[#out+1] = i18n.acquisition.area
                out[#out+1]= '<br>'
                out[#out+1] = tostring(ul)
             end
             end
            out[#out+1] = h.head(i18n.acquisition.area_header)
            out[#out+1] = i18n.acquisition.area
            out[#out+1] = tostring(ul)
         end
         end
     end
     end
Line 556: Line 508:
     -- Drop restrictions by monster
     -- Drop restrictions by monster
     -- ------------------------------------------------------------------------
     -- ------------------------------------------------------------------------
     local monster_metadata_ids = {}
     local monster_metadata_ids = {}
     if item_data['items.drop_monsters'] then
     if item_data.drop_monsters then
         monster_metadata_ids = m_util.string.split(item_data['items.drop_monsters'], ',%s*')
         monster_metadata_ids = m_util.cast.table(item_data.drop_monsters)
     end
     end
              
              
Line 590: Line 541:
                         row['monsters.name']
                         row['monsters.name']
                     ))
                     ))
             end          
             end
             out[#out+1] = h.head(i18n.acquisition.monster_header)
             out[#out+1] = h.head(i18n.acquisition.monster_header)
             out[#out+1] = i18n.acquisition.monster
             out[#out+1] = i18n.acquisition.monster
Line 599: Line 550:
      
      
     -- ------------------------------------------------------------------------
     -- ------------------------------------------------------------------------
     -- Vendor recipes/upgrades handling
     -- Recipes section
     -- ------------------------------------------------------------------------
     -- ------------------------------------------------------------------------
      
      
     --
     --
     -- Query set data
     -- Query recipe data
     --
     --
     local obtained_sets, obtained_sets_order = h.fetch_upgraded_from_sets(
     local obtained_sets, obtained_sets_order = h.fetch_recipes(
         string.format('upgraded_from_sets._pageName="%s"', tpl_args.page),
         string.format('acquisition_recipes._pageID="%s"', item_data._pageID),
         string.format('upgraded_from_groups._pageName="%s"', tpl_args.page)
         string.format('acquisition_recipe_parts._pageID="%s"', item_data._pageID)
     )
     )
      
      
     results = m_cargo.query(
     -- Find recipes that item is used in
        {'upgraded_from_groups'},
    local tables = {'acquisition_recipe_parts'}
        {
    local fields = {
            'upgraded_from_groups._pageID',
        '_pageID',
            'upgraded_from_groups.set_id',
        'recipe_id',
        },
    }
        {
    local query = {
            where=string.format('upgraded_from_groups.item_page="%s"', tpl_args.page),
        where = string.format('item_page="%s"', item_data._pageName),
            -- only need one result set for the where clause
        groupBy = '_pageID, recipe_id',
            groupBy='upgraded_from_groups._pageID, upgraded_from_groups.set_id',
    }
        }
 
     )
    -- Namespace condition
     local where = {sets={}, groups={}}
     -- This is mainly to prevent items from user pages or other testing pages
    for key, data in pairs(where) do
    -- from being returned in the query results.
         for _, row in ipairs(results) do
     if tpl_args.namespaces ~= 'any' then
             data[#data+1] = string.format('upgraded_from_%s.set_id="%s" AND upgraded_from_%s._pageID="%s"', key, row['upgraded_from_groups.set_id'], key, row['upgraded_from_groups._pageID'])
        local namespaces = m_util.cast.table(tpl_args.namespaces, {callback=m_util.cast.number})
         if #namespaces > 0 then
             namespaces = table.concat(namespaces, ',')
        else
            namespaces = m_item_util.get_item_namespaces{format = 'list'}
         end
         end
         where[key] = table.concat(data, ' OR ')
         query.where = string.format('%s AND _pageNamespace IN (%s)', query.where, namespaces)
    end
 
    local results = m_cargo.query(tables, fields, query)
    local recipes = {}
    local parts = {}
    for _, row in ipairs(results) do
        recipes[#recipes+1] = string.format('acquisition_recipes._pageID="%s" AND acquisition_recipes.recipe_id="%s"', row._pageID, row.recipe_id)
        parts[#parts+1] = string.format('acquisition_recipe_parts._pageID="%s" AND acquisition_recipe_parts.recipe_id="%s"', row._pageID, row.recipe_id)
     end
     end
     local ingredient_sets, ingredient_sets_order = h.fetch_upgraded_from_sets(where.sets, where.groups)
    parts = table.concat(parts, ' OR ')
    recipes = table.concat(recipes, ' OR ')
 
     local ingredient_sets, ingredient_sets_order = h.fetch_recipes(recipes, parts)
     --
     --
     -- Query bulk item info for item linking
     -- Query bulk item info for item linking
Line 638: Line 604:
             local set = set_data[1][set_key]
             local set = set_data[1][set_key]
             for _, group in ipairs(set.groups) do
             for _, group in ipairs(set.groups) do
                 item_pages.assoc[group['upgraded_from_groups.item_page']] = true
                 item_pages.assoc[group['acquisition_recipe_parts.item_page']] = true
             end
             end
             item_pages.assoc[set['upgraded_from_sets._pageName']] = true
             item_pages.assoc[set['acquisition_recipes._pageName']] = true
         end
         end
     end
     end
Line 648: Line 614:
     end
     end
      
      
     if #item_pages < c.MAX_ITEMS then
     if #item_pages < cfg.MAX_ITEMS then
         item_pages = m_cargo.map_results_to_id{
         item_pages = m_cargo.map_results_to_id{
             results=m_cargo.array_query{
             results=m_cargo.array_query{
Line 668: Line 634:
     --
     --
      
      
     h.upgraded_from_table(tpl_args, obtained_sets, obtained_sets_order, 'obtained', out, item_pages)
     h.recipe_table(tpl_args, item_data, obtained_sets, obtained_sets_order, 'obtained', out, item_pages)
      
      
     --  
     --  
Line 674: Line 640:
     --  
     --  
      
      
     h.upgraded_from_table(tpl_args, ingredient_sets, ingredient_sets_order, 'ingredient', out, item_pages)
     h.recipe_table(tpl_args, item_data, ingredient_sets, ingredient_sets_order, 'ingredient', out, item_pages)
      
      
     out[#out+1] = tpl_args.ingredient_append
     out[#out+1] = tpl_args.ingredient_append
Line 693: Line 659:
         },
         },
         {
         {
             where=string.format('quest_rewards.reward="%s"', tpl_args.page),
             where=string.format('quest_rewards._pageName="%s"', item_data._pageName),
             orderBy='quest_rewards.act ASC, quest_rewards.quest_id ASC',
             orderBy='quest_rewards.act ASC, quest_rewards.quest_id ASC',
         }
         }
Line 708: Line 674:
         },
         },
         {
         {
             where=string.format('vendor_rewards.reward="%s"', tpl_args.page),
             where=string.format('vendor_rewards._pageName="%s"', item_data._pageName),
             orderBy='vendor_rewards.act ASC, vendor_rewards.quest_id ASC',
             orderBy='vendor_rewards.act ASC, vendor_rewards.quest_id ASC',
         }
         }
Line 724: Line 690:


-- ----------------------------------------------------------------------------
-- ----------------------------------------------------------------------------
-- Return
-- Exported functions
-- ----------------------------------------------------------------------------
-- ----------------------------------------------------------------------------
local p = {}
--
-- Template: Item acquisition
--
p.main = m_util.misc.invoker_factory(_main, {
    wrappers = cfg.wrapper,
})
p.item_acquisition = p.main


return p
return p

Latest revision as of 18:19, 23 October 2024

Module documentation[view] [edit] [history] [purge]


This module implements {{item acquisition}}.

-------------------------------------------------------------------------------
-- 
--                           Module:Item acquisition
-- 
-- This module implements Template:Item acquisition.
-------------------------------------------------------------------------------

require('Module:No globals')
local m_util = require('Module:Util')
local m_cargo = require('Module:Cargo')
local m_item_util = require('Module:Item util')
local m_quest_reward = require('Module:Quest reward')._shared

local m_game = mw.loadData('Module:Game')

-- Should we use the sandbox version of our submodules?
local use_sandbox = m_util.misc.maybe_sandbox('Item acquisition')

-- Lazy loading
local f_item_link -- require('Module:Item link').item_link

-- The cfg table contains all localisable strings and configuration, to make it
-- easier to port this module to another wiki.
local cfg = use_sandbox and mw.loadData('Module:Item acquisition/config/sandbox') or mw.loadData('Module:Item acquisition/config')

local i18n = cfg.i18n

-- ----------------------------------------------------------------------------
-- Helper functions
-- ----------------------------------------------------------------------------

local h = {}

-- Lazy loading for Module:Item link
function h.item_link(args)
    if not f_item_link then
        f_item_link = require('Module:Item link').item_link
    end
    return f_item_link(args)
end

function h.head(str, level)
    local head = mw.html.create('h' .. (level or 3))
    head:wikitext(str)
    return tostring(head)
end

function h.fetch_recipes(where_set, where_group)
    if where_set == "" or where_group == "" then
        return {}, {}
    end
    
    local sets = {}
    local results = m_cargo.query(
        {'acquisition_recipes'},
        {
            'acquisition_recipes._pageName',
            'acquisition_recipes.recipe_id',
            'acquisition_recipes.result_amount',
            'acquisition_recipes.description',
            'acquisition_recipes.automatic',
        },
        {
            where=where_set,
        }
    )
    
    for _, row in ipairs(results) do
        row.groups = {}
        sets[row['acquisition_recipes._pageName'] .. tonumber(row['acquisition_recipes.recipe_id'])] = row
    end
    
    results = m_cargo.query(
        {'acquisition_recipe_parts'},
        {
            'acquisition_recipe_parts._pageName',
            'acquisition_recipe_parts.recipe_id',
            'acquisition_recipe_parts.part_id',
            'acquisition_recipe_parts.notes',
            'acquisition_recipe_parts.amount',
            'acquisition_recipe_parts.item_name',
            'acquisition_recipe_parts.item_page',
        },
        {
            where=where_group,
        }
    )
    
    for _, row in ipairs(results) do
        sets[row['acquisition_recipe_parts._pageName'] .. tonumber(row['acquisition_recipe_parts.recipe_id'])].groups[tonumber(row['acquisition_recipe_parts.part_id'])] = row
    end
    
    local sets_sort = {}
    for key, _ in pairs(sets) do
        sets_sort[#sets_sort+1] = key
    end
    
    table.sort(sets_sort, function (a, b)
        return tonumber(sets[a]['acquisition_recipes.recipe_id']) < tonumber(sets[b]['acquisition_recipes.recipe_id'])
    end)
    
    return sets, sets_sort
end

function h.recipe_table(tpl_args, item, sets, set_order, data_type, out, item_pages)
    
    -- Count the number of item pages:
    local item_pages_count = 0
    for _,__ in pairs(item_pages) do 
        item_pages_count = item_pages_count + 1
    end
    
    -- Avoid creating headers only when no data is given
    if #set_order <= 0 then
        return
    end
    
    -- Prevent showing either group or set notes when no rows have any to save a bit of vertical space
    local set_notes = false
    local group_notes = false
    for _, set_key in ipairs(set_order) do
        if set_notes and group_notes then
            break
        end
        local set = sets[set_key]
        if set['acquisition_recipes.description'] ~= nil then
            set_notes = true
        end
        
        if #set.groups > 0 then
            for i, group in ipairs(set.groups) do
                if group['acquisition_recipe_parts.notes'] ~= nil then
                    group_notes = true
                    break
                end
            end
        end
    end
    
    local tbl = mw.html.create('table')
    
    -- sorting will mess up the table because of the rowspawns
    local table_class = 'wikitable responsive-table'
    --[[if item_pages_count > cfg.COLLAPSE_TABLE then
        table_class = 'wikitable mw-collapsible mw-collapsed'
    end--]]
    tbl:attr('class', table_class)
    
    --
    -- Header
    -- 
    local tr = tbl:tag('tr')
    if data_type == 'ingredient' then
        tr
            :tag('th')
                :wikitext(i18n.recipe_table.result)
    end
    
    tr
        :tag('th')
            :wikitext(i18n.recipe_table.part_amount)
            :done()
        :tag('th')
            :wikitext(i18n.recipe_table.part)
            :done()
    if group_notes then
        tr
            :tag('th')
                :wikitext(i18n.recipe_table.part_notes)
                :done()
    end
    if set_notes then
         tr
            :tag('th')
                :wikitext(i18n.recipe_table.description)
                :done()
    end
    tr
        :tag('th')
            :wikitext(i18n.recipe_table.meta)
            :done()
    --
    -- Rows
    --
    
    for _, set_key in ipairs(set_order) do
        local set = sets[set_key]
        
        if #set.groups > 0 then
            tr = tbl:tag('tr')
            tr:attr('class', 'upgraded-from-set')
            
            if data_type == 'ingredient' then
                local str
                if tpl_args.no_link == nil and item_pages_count < cfg.MAX_ITEMS then
                    local item_data = item_pages[set['acquisition_recipes._pageName']][1]
                    str = h.item_link{page=set['acquisition_recipes._pageName'], name=item_data['items.name'], inventory_icon=item_data['items.inventory_icon'] or '', html=item_data['items.html'] or '', skip_query=true}
                else
                    str = string.format('[[%s]]', set['acquisition_recipes._pageName'])
                end
                
                tr
                    :tag('td')
                        :attr('rowspan', #set.groups)
                        :wikitext(str)
                        :done()
            end
            
            local tr2
            for i, group in ipairs(set.groups) do
                if i <= 1 then
                    tr2 = tr
                else
                    tr2 = tbl:tag('tr')
                end
                local str
                if tpl_args.no_link == nil and item_pages_count < cfg.MAX_ITEMS then
                    local item_data = item_pages[group['acquisition_recipe_parts.item_page']][1]
                    str = h.item_link{
                        page=group['acquisition_recipe_parts.item_page'], 
                        name=item_data['items.name'], 
                        inventory_icon=item_data['items.inventory_icon'] or '', 
                        html=item_data['items.html'] or '', 
                        skip_query=true
                    }
                else
                    str = string.format('[[%s]]', group['acquisition_recipe_parts.item_page'])
                end
                
                tr2
                    :tag('td')
                        :attr('data-sort-type', 'number')
                        :wikitext(group['acquisition_recipe_parts.amount'])
                        :done()
                    :tag('td')
                        :wikitext(str)
                        :done()
                
                if group_notes then
                    if group['acquisition_recipe_parts.notes'] then
                        tr2
                            :tag('td')
                                :wikitext(group['acquisition_recipe_parts.notes'])
                    else
                        tr2
                            :node(
                                m_util.html.table_cell('na')
                            )
                    end
                end
            end
            
            if set_notes then
                if set['acquisition_recipes.description'] then
                    tr
                        :tag('td')
                            :attr('rowspan', #set.groups)
                            :wikitext(set['acquisition_recipes.description'])
                else
                    tr
                        :node(
                            m_util.html.table_cell('na')
                                :attr('rowspan', #set.groups)
                        )
                end
            end
            
            local t
            if set['acquisition_recipes.automatic'] == '1' then
                t = i18n.recipe_table.automatic
            else
                t = i18n.recipe_table.manual
            end
            
            tr
                :tag('td')
                    :attr('rowspan', #set.groups)
                    :wikitext(t)
        end
    end
    
    
    --
    -- print
    --
    
    if data_type == 'ingredient' then
        out[#out+1] = h.head(i18n.acquisition.recipe_usage_heading)
        out[#out+1] = string.format(i18n.acquisition.recipe_usage_text, item.name)
    else
        out[#out+1] = h.head(i18n.acquisition.recipes_heading)
        out[#out+1] = string.format(i18n.acquisition.recipes_text, item.name)
    end
    out[#out+1] = '<br>'
    out[#out+1] = tostring(tbl)
end

function h.reward_table(data, rtbl)
    if #data == 0 then
        return
    end
    
    local tbl = m_quest_reward.reward_tbl_head()
            
    for _, row in ipairs(data) do
        local classes
        if row[rtbl .. '.classes'] then
            classes = {}
            for _, class in ipairs(m_util.string.split(row[rtbl .. '.classes'], ',')) do
                classes[class] = true
            end
        end
        
        local tr = tbl:tag('tr')
        m_quest_reward.reward_tbl_row_head(tr, rtbl, row)

        if classes then
            for _, class in ipairs(m_game.constants.characters_order) do
                if classes[m_game.constants.characters[class].name] then
                    tr
                        :node(
                            m_util.html.table_cell('yes')
                        )
                else
                    tr
                        :node(
                            m_util.html.table_cell('no')
                        )
                end
            end
        else
            tr
                :node(
                    m_util.html.table_cell('yes')
                        :attr('colspan', 7)
                )
        end
    end
    
    return h.head(i18n.quest_reward[rtbl .. '_header']) .. i18n.quest_reward[rtbl .. '_intro'] .. tostring(tbl)
end

-- ----------------------------------------------------------------------------
-- Main functions
-- ----------------------------------------------------------------------------

local function _main(tpl_args)
    --[[
    Duplicates the information from the infobox in a more readable 
    manner on the page.
    
    Example
    -------
    = p.item_acquisition{page='The Rite of Elements'}
    ]]

    local out = {}
    out[#out+1] = tpl_args.acquisition_insert
    
    -- fetch general item drop information that is used in multiple places in a
    -- single query to reduce performance hit
    local tables = {'items'}
    local fields = {
        'items._pageID=_pageID',
        'items._pageName=_pageName',
        'items.name=name',
        'items.class_id=class_id',
        'items.rarity_id=rarity_id',
        'items.base_item_page=base_item_page',
        'items.drop_text=drop_text',
        'items.acquisition_tags=acquisition_tags',
        'items.drop_areas=drop_areas',
        'items.drop_monsters=drop_monsters',
        'items.drop_enabled=drop_enabled',
        'items.is_drop_restricted=is_drop_restricted',
    }
    local query = {
        limit = 1,
    }
    if tpl_args.page then
        -- Join with _pageData in order to check for page redirect
        tables[#tables+1] = '_pageData'
        query.join = 'items._pageName = _pageData._pageNameOrRedirect'
        query.where = string.format(
            '_pageData._pageName="%s"',
            tpl_args.page
        )
    else
        query.where = string.format(
            'items._pageName="%s"',
            tostring(mw.title.getCurrentTitle())
        )
    end
    local item_data = m_cargo.query(tables, fields, query)
    if #item_data > 0 then
        item_data = item_data[1]
    end
    if item_data == nil then
        error("Item search unexpectedly returned no results")
    elseif item_data.name == nil then
        error("Item search result unexpectedly has no name")
    end
    
    -- ------------------------------------------------------------------------
    -- General drop restrictions
    -- ------------------------------------------------------------------------
    local drop_enabled = m_util.cast.boolean(item_data.drop_enabled)
    local drop_restricted = m_util.cast.boolean(item_data.is_drop_restricted)
    local is_unique = item_data.rarity_id == 'unique'
    if not drop_enabled then
        out[#out+1] = string.format(i18n.acquisition.drop_disabled, item_data.name)
    elseif drop_restricted then
        -- Acquisition unkown
        if not item_data.drop_areas and not item_data.drop_monsters and not item_data.drop_text then
            -- Divination cards
            if item_data.class_id == 'DivinationCard' then
                out[#out+1] = mw.getCurrentFrame():expandTemplate{title = i18n.templates.divination_card_acquisition_unknown}
                out[#out+1] = '\n\n'
            end
        end

        out[#out+1] = string.format(i18n.acquisition.drop_restricted, item_data.name)
        if is_unique then
            out[#out+1] = ' '
            out[#out+1] = i18n.acquisition.cannot_be_chanced
        end
    else
        if not item_data.drop_areas then
            out[#out+1] = string.format(i18n.acquisition.drop_anywhere, item_data.name)
        end
        if is_unique then
            local base_item_data = m_cargo.query(
                {'items'},
                {
                    'items.is_corrupted=is_corrupted',
                    'items.drop_enabled=drop_enabled',
                },
                {
                    where = string.format('items._pageName="%s"', item_data.base_item_page),
                    groupBy = 'items._pageID',
                }
            )
            if #base_item_data > 0 then
                base_item_data = base_item_data[1]
                if m_util.cast.boolean(base_item_data.drop_enabled) and not m_util.cast.boolean(base_item_data.is_corrupted) then
                    out[#out+1] = ' '
                    out[#out+1] = i18n.acquisition.can_be_chanced
                end
            end
        end
    end
    
    -- ------------------------------------------------------------------------
    -- League-specific
    -- ------------------------------------------------------------------------
    local acquisition_tags = {}
    if item_data.acquisition_tags then
        acquisition_tags = m_util.cast.table(item_data.acquisition_tags)
    end
    if m_util.table.contains(acquisition_tags, 'league-specific') and drop_enabled and is_unique then
        out[#out+1] = '\n\n'
        out[#out+1] = string.format(i18n.acquisition.league_specific_unique, item_data.name)
    end
    
    -- ------------------------------------------------------------------------
    -- Drop restrictions by text
    -- ------------------------------------------------------------------------
    if item_data.drop_text and drop_enabled then
        out[#out+1] = '\n\n'
        out[#out+1] = item_data.drop_text
    end
    
    -- ------------------------------------------------------------------------
    -- Drop restrictions by area
    -- ------------------------------------------------------------------------
    local drop_areas = {}
    if item_data.drop_areas then
        drop_areas = m_util.cast.table(item_data.drop_areas)
    end
    if #drop_areas > 0 then
        local results = m_cargo.query(
            {'areas'},
            {
                'areas._pageName=_pageName',
                'areas.id=id',
                'areas.name=name',
                'areas.main_page=main_page',
            },
            {
                where = string.format('areas.id IN ("%s") AND NOT areas.is_legacy_map_area', table.concat(drop_areas, '","')),
                orderBy = 'areas.name ASC',
                groupBy = 'areas._pageID',
            }
        )
        if #results > 0 then
            local ul = mw.html.create('ul')
            for _, row in ipairs(results) do
                ul:tag('li')
                    :wikitext(m_util.html.wikilink(row.main_page or row._pageName, row.name))
            end
            out[#out+1] = h.head(i18n.acquisition.area_header)
            out[#out+1] = i18n.acquisition.area
            out[#out+1] = tostring(ul)
        end
    end
    
    -- ------------------------------------------------------------------------
    -- Drop restrictions by monster
    -- ------------------------------------------------------------------------
    local monster_metadata_ids = {}
    if item_data.drop_monsters then
        monster_metadata_ids = m_util.cast.table(item_data.drop_monsters)
    end
            
    if #monster_metadata_ids > 0 then
        local results = m_cargo.query(
            {'monsters', 'main_pages'},
            {
                'monsters._pageName',
                'monsters.metadata_id',
                'monsters.name',
                -- 'monsters.main_page',
                'main_pages._pageName',
            },
            {
                join='monsters.metadata_id=main_pages.id',
                where=string.format('monsters.metadata_id IN ("%s")', table.concat(monster_metadata_ids, '","')),
                orderBy='monsters.name',
                groupBy='monsters.metadata_id',
            }
        )
        if #results > 0 then
            local ul = mw.html.create('ul')
            for _, row in ipairs(results) do
                ul:tag('li')
                    :wikitext(string.format(
                        '[[%s|%s]]', 
                           row['monsters.main_page'] 
                        or row['main_pages._pageName'] 
                        or row['monsters._pageName'], 
                        row['monsters.name']
                    ))
            end
            out[#out+1] = h.head(i18n.acquisition.monster_header)
            out[#out+1] = i18n.acquisition.monster
            out[#out+1]= '<br>'
            out[#out+1] = tostring(ul)
        end
    end
    
    -- ------------------------------------------------------------------------
    -- Recipes section
    -- ------------------------------------------------------------------------
    
    --
    -- Query recipe data
    --
    local obtained_sets, obtained_sets_order = h.fetch_recipes(
        string.format('acquisition_recipes._pageID="%s"', item_data._pageID),
        string.format('acquisition_recipe_parts._pageID="%s"', item_data._pageID)
    )
    
    -- Find recipes that item is used in
    local tables = {'acquisition_recipe_parts'}
    local fields = {
        '_pageID',
        'recipe_id',
    }
    local query = {
        where = string.format('item_page="%s"', item_data._pageName),
        groupBy = '_pageID, recipe_id',
    }

    -- Namespace condition
    -- This is mainly to prevent items from user pages or other testing pages 
    -- from being returned in the query results.
    if tpl_args.namespaces ~= 'any' then
        local namespaces = m_util.cast.table(tpl_args.namespaces, {callback=m_util.cast.number})
        if #namespaces > 0 then
            namespaces = table.concat(namespaces, ',')
        else
            namespaces = m_item_util.get_item_namespaces{format = 'list'}
        end
        query.where = string.format('%s AND _pageNamespace IN (%s)', query.where, namespaces)
    end

    local results = m_cargo.query(tables, fields, query)
    local recipes = {}
    local parts = {}
    for _, row in ipairs(results) do
        recipes[#recipes+1] = string.format('acquisition_recipes._pageID="%s" AND acquisition_recipes.recipe_id="%s"', row._pageID, row.recipe_id)
        parts[#parts+1] = string.format('acquisition_recipe_parts._pageID="%s" AND acquisition_recipe_parts.recipe_id="%s"', row._pageID, row.recipe_id)
    end
    parts = table.concat(parts, ' OR ')
    recipes = table.concat(recipes, ' OR ')

    local ingredient_sets, ingredient_sets_order = h.fetch_recipes(recipes, parts)
    --
    -- Query bulk item info for item linking
    --
    local item_pages = {assoc={}}
    for _, set_data in ipairs({{obtained_sets, obtained_sets_order}, {ingredient_sets, ingredient_sets_order}}) do
        for _, set_key in ipairs(set_data[2]) do
            local set = set_data[1][set_key]
            for _, group in ipairs(set.groups) do
                item_pages.assoc[group['acquisition_recipe_parts.item_page']] = true
            end
            item_pages.assoc[set['acquisition_recipes._pageName']] = true
        end
    end
    -- remove duplicates
    for name, _ in pairs(item_pages.assoc) do
        item_pages[#item_pages+1] = name
    end
    
    if #item_pages < cfg.MAX_ITEMS then
        item_pages = m_cargo.map_results_to_id{
            results=m_cargo.array_query{
                tables={'items'},
                fields={'items.name', 'items.inventory_icon', 'items.html'},
                id_array = item_pages,
                id_field = 'items._pageName',
                query = {
                    limit=5000
                },
            },
            field='items._pageName',
            keep_id_field=true,
        }
    end
    
    -- 
    -- Output for being obtained via vendor recipe
    --
    
    h.recipe_table(tpl_args, item_data, obtained_sets, obtained_sets_order, 'obtained', out, item_pages)
    
    -- 
    -- Ingredient of vendor recipes/upgrades
    -- 
    
    h.recipe_table(tpl_args, item_data, ingredient_sets, ingredient_sets_order, 'ingredient', out, item_pages)
    
    out[#out+1] = tpl_args.ingredient_append
    
    -- ------------------------------------------------------------------------
    -- Obtained via quest or vendor reward
    -- ------------------------------------------------------------------------
    
    local quest_rewards = m_cargo.query(
        {'quest_rewards'},
        {
            'quest_rewards.quest',
            'quest_rewards.act',
            'quest_rewards.classes',
            'quest_rewards.sockets',
            'quest_rewards.item_level',
            'quest_rewards.rarity',
        },
        {
            where=string.format('quest_rewards._pageName="%s"', item_data._pageName),
            orderBy='quest_rewards.act ASC, quest_rewards.quest_id ASC',
        }
    )
    out[#out+1] = h.reward_table(quest_rewards, 'quest_rewards')
    
    local vendor_rewards = m_cargo.query(
        {'vendor_rewards'},
        {
            'vendor_rewards.quest',
            'vendor_rewards.act',
            'vendor_rewards.classes',
            'vendor_rewards.npc',
        },
        {
            where=string.format('vendor_rewards._pageName="%s"', item_data._pageName),
            orderBy='vendor_rewards.act ASC, vendor_rewards.quest_id ASC',
        }
    )
    out[#out+1] = h.reward_table(vendor_rewards, 'vendor_rewards')
    
    -- ------------------------------------
    -- output
    -- ------------------------------------
    
    local head = mw.html.create('h2')
    head:wikitext(i18n.acquisition.header .. i18n.acquisition.link)
    return tostring(head) .. table.concat(out)
end

-- ----------------------------------------------------------------------------
-- Exported functions
-- ----------------------------------------------------------------------------

local p = {}

--
-- Template: Item acquisition
--
p.main = m_util.misc.invoker_factory(_main, {
    wrappers = cfg.wrapper,
})

p.item_acquisition = p.main

return p