Module:Modifier table: Difference between revisions

From Path of Exile 2 Wiki
Jump to navigation Jump to search
>Illviljan
(Looking at poe.trade shows that attack speed from explicit and implicit can spawn together on 1h axes, separated the mod group counter into explicit and implicit as well.)
No edit summary
 
(48 intermediate revisions by 7 users not shown)
Line 1: Line 1:
--[[
-------------------------------------------------------------------------------
Module responsible for displaying modifiers in various ways.
--
 
--                          Module:Modifier table
]]
--
-- Module responsible for displaying modifiers in various ways. Implements
-- Template:Modifier table and Template:Item modifiers
-------------------------------------------------------------------------------


require('Module:No globals')
local m_util = require('Module:Util')
local m_util = require('Module:Util')
local getArgs = require('Module:Arguments').getArgs
local m_game = require('Module:Game')
local f_item_link = require('Module:Item link').item_link
local m_cargo = require('Module:Cargo')
local m_cargo = require('Module:Cargo')


local cargo = mw.ext.cargo
-- Should we use the sandbox version of our submodules?
local use_sandbox = m_util.misc.maybe_sandbox('Modifier table')


local p = {}
local m_game = mw.loadData('Module:Game')
 
-- 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:Modifier table/config/sandbox') or mw.loadData('Module:Modifier table/config')
 
local i18n = cfg.i18n


-- ----------------------------------------------------------------------------
-- ----------------------------------------------------------------------------
-- Strings
-- Helper functions
-- ----------------------------------------------------------------------------
-- ----------------------------------------------------------------------------
-- This section contains strings used by this module.
-- 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.
local i18n = {
    mod_table = {
        name = m_util.html.abbr('Name', 'Name of the modifier if available or its internal identifier instead'),
        mod_group = m_util.html.abbr('Group', 'Only one modifier from the specified group can appear at a time under normal circumstances'),
        mod_type = 'Type',
        domain = '[[Modifiers#Mod_Domain|Domain]]',
        generation_type = '[[Modifiers#Mod_Generation_Type|Generation Type]]',
        required_level = '[[Image:Level_up_icon_small.png|link=|For generated item/monster modifiers the minimum item/monster level respectively. Some generation types may not require this condition to be met, however item level restrictions may be raised to 80% of this value.]]',
        labyrinth = '[[The Lord\'s Labyrinth|Labyrinth]]',
        stat_text = m_util.html.abbr('Stats', 'Stats of the modifier and the range they can roll in (if applicable)'),
        buff = m_util.html.abbr('Buff', 'ID of the buff granted and the values associated'),
        granted_skill = m_util.html.abbr('Skill', 'ID of the skill granted'),
        tags = '[[Tags]]',
       
        iiq = m_util.html.abbr('IIQ', 'increased Quantity of Items found in this Area'),
        iir = m_util.html.abbr('IIR', 'increased Rarity of Items found in this Area'),
        pack_size = m_util.html.abbr('Pack<br>Size', 'Monster pack size'),
       
        spawn_weights = m_util.html.abbr('Spawn Weighting', 'List of applicable tags and their values for weighting (i.e. calculating the chances) of the spawning of this modifier'),
        generation_weights = m_util.html.abbr('Generation Weighting', 'List of applicable tags and their values for weighting (i.e. calculating the chances) of the spawning of this modifier'),
       
        normal_labyrinth = 'Normal Labyrinth',
        cruel_labyrinth = 'Cruel Labyrinth',
        merciless_labyrinth = 'Merciless Labyrinth',
        eternal_labyrinth = 'Eternal Labyrinth',
    },
   
    drop_down_table = {
        collapse_all = 'Collapse all',
        expand_all = 'Expand all',
        table_intro = 'The table below displays the available [[modifiers]] for [[item]]s such as',
        prefix = 'Prefix',
        suffix = 'Suffix',
        corrupted = 'Corrupted',   
        enchant = 'Enchantment',
        elder_prefix = 'Elder prefix',
        elder_suffix = 'Elder suffix',
        shaper_prefix = 'Shaper prefix',
        shaper_suffix = 'Shaper suffix',
        delve_prefix = 'Delve prefix',
        delve_suffix = 'Delve suffix',
        mod_group = 'Mod group:',
    },
   
    errors = {
        --
        -- Mod template
        --
        sell_price_duplicate_name = 'Do not specify a sell price item name multiple times. Adjust the amount instead.',
        sell_price_missing_argument = 'Both %s and %s must be specified',
       
        --
        -- Modifier link template
        --
        undefined_statid = 'Please define any of these stat ids: %s',
        incorrect_modid = 'Please change the name from "%s" to any of these modifier ids:<br>%s',
        multiple_results = 'Please choose only one of these modifier ids:<br>%s',
        no_results = 'No results found.',
    },
}
--
-- Helper/Utility functions
--


local h = {}
local h = {}


function h.header(str)
function h.query_weights(table_name, page_ids)
     --[[
     return m_cargo.map_results_to_id{
    This function replaces specific numbers with a generic #.  
        results=m_cargo.query(
    ]]
            {'mods', table_name},
   
            {
    local s = table.concat(m_util.string.split(str, '%(%d+%.*%d*%-%d+%.*%d*%)'), '#')
                'mods._pageID',
    s = table.concat(m_util.string.split(s, '%d+%.*%d*'), '#')
                table_name .. '.tag',
    s = table.concat(m_util.string.split(s, '<br>'), ', ')
                table_name .. '.value'
    s = table.concat(m_util.string.split(s, '<br />'), ' ')
            },
 
            {
  return s
                where=page_ids,
                join=string.format('mods._pageID=%s._pageID', table_name),
                orderBy=string.format('mods.id ASC,%s.ordinal ASC', table_name),
            }
        ),
        field='mods._pageID',
    }
end
end


function h.query_weights(table_name, page_ids)
h.tbl = {}
    results = cargo.query(
h.tbl.display = {}
        string.format('mods,%s', table_name),
h.tbl.display.factory = {}
        string.format('mods._pageID,%s.tag,%s.weight', table_name, table_name),
function h.tbl.display.factory.value(args)
        {
     -- Format options for each field:
            where=page_ids,
     args.options = args.options or {}
            join=string.format('mods._pageID=%s._pageID', table_name),
 
            orderBy=string.format('mods.id ASC,%s.ordinal ASC', table_name),
     -- Separator between fields:
            limit=5000,
     args.delimiter = args.delimiter or ', '
        }
     )
     if #results == 5000 then
        error('Hit maximum cargo results')
     end
      
    return m_util.cargo.map_results_to_id{results=results,field='mods._pageID'}
end


function h.disambiguate_mod_name(results)
    return function(tpl_args, tr, data, fields)
    --[[
         local values = {}
         Disambiguates results from a mods query.
         local fmt_values = {}
    ]]
    local str = {}
    for i,v in pairs(results) do
         str[#str+1] = string.format(
            '%s - %s ([[%s|page]])',
            v['mods.id'] or v['mods._pageName'] or '',
            string.gsub(
                v['mods.stat_text_raw'] or 'N/A',
                '<br>',
                ', '
            ) or '',
            v['mods._pageName'] or ''
        )
    end
    return table.concat(str, '<br>')
end


function h.cargo_query(tpl_args)
        for index, field in ipairs(fields) do
    --[[
            local value = {
    Returns a Cargo query of all the results.
                min=data[field],
   
                max=data[field],
    tpl_args to be added to the cargo table needs to prefixed with q_.
                base=data[field],
    * tpl_args.q_*
            }
   
            if value.min then
    ]]
                values[#values+1] = value.max
   
                local opts = args.options[index] or {}
    -- Parse query arguments
                -- Global colour is set, no overrides.
    local query = {
                if args.color ~= nil or opts.color == nil then
        limit = 5000,
                    opts.no_color = true
        offset = 0,
                end
    }
                fmt_values[#fmt_values+1] = m_util.html.format_value(nil, value, opts)
    for key, value in pairs(tpl_args) do
             end
        if string.sub(key, 0, 2) == 'q_' then
             query[string.sub(key, 3)] = value
         end
         end
    end
   
    -- Query cargo table. If there are too many results then repeat,
    -- offset, re-query and add the remaining results:
    local results = {}     
    repeat
        local result = cargo.query(
            query.tables,
            query.fields,
            query
        )
        query.offset = query.offset + #result


         for _,v in ipairs(result) do
         if #values == 0 then
             results[#results + 1] = v
            tr
                :node(m_util.html.table_cell('na'))
        else
            local td = tr:tag('td')
            td
                :attr('data-sort-value', table.concat(values, args.delimiter))
                :wikitext(table.concat(fmt_values, args.delimiter))
            if args.color then
                td:attr('class', 'tc -' .. args.color)
             end
         end
         end
     until #result < query.limit
     end
   
    return results
end
end


-- ----------------------------------------------------------------------------
-- ----------------------------------------------------------------------------
-- Template: Mod table
-- Additional configuration
-- ----------------------------------------------------------------------------
-- ----------------------------------------------------------------------------


Line 195: Line 108:
           },
           },
         },
         },
         display = function(tpl_args, frame, tr, data)
         display = function(tpl_args, tr, data, fields)
             local name
             local name
             if data['mods.name'] then
             if data['mods.name'] then
Line 202: Line 115:
                 name = data['mods.id']
                 name = data['mods.id']
             end
             end
             tr
             tr
                 :tag('td')
                 :tag('td')
Line 213: Line 127:
         header = i18n.mod_table.domain,
         header = i18n.mod_table.domain,
         fields = {'mods.domain'},
         fields = {'mods.domain'},
         display = function(tpl_args, frame, tr, data)
         display = function(tpl_args, tr, data, fields)
             local value = data['mods.domain']
             local k = 'mods.domain'
             tr
            local i = tonumber(data[k])
                 :tag('td')
             if m_game.constants.mod.domains[i] == nil then
                    :attr('data-sort-value', value)
                 error('Undefined Modifier Domain ['..i..'] needs to be added to Module:Game')
                    :wikitext(m_game.constants.mod.domains[tonumber(value)]['short_upper'])
            end
            data[k] = m_game.constants.mod.domains[i]['short_upper']
            h.tbl.display.factory.value{}(tpl_args, tr, data, fields)
         end,
         end,
         order = 2000,
         order = 2000,
Line 227: Line 143:
         header = i18n.mod_table.generation_type,
         header = i18n.mod_table.generation_type,
         fields = {'mods.generation_type'},
         fields = {'mods.generation_type'},
         display = function(tpl_args, frame, tr, data)
         display = function(tpl_args, tr, data, fields)
             local value = data['mods.generation_type']
             local k = 'mods.generation_type'
             tr
            local i = tonumber(data[k])
                 :tag('td')
             if m_game.constants.mod.generation_types[i] == nil then
                    :attr('data-sort-value', value)
                 error('Undefined Modifier Generation Type ['..i..'] needs to be added to Module:Game')
                    :wikitext(m_game.constants.mod.generation_types[tonumber(value)]['short_upper'])
            end
            data[k] = m_game.constants.mod.generation_types[i]['short_upper']
            h.tbl.display.factory.value{}(tpl_args, tr, data, fields)
         end,
         end,
         order = 2001,
         order = 2001,
Line 239: Line 157:
     {
     {
         arg = {'group', 'mod_group'},
         arg = {'group', 'mod_group'},
         header = i18n.mod_table.mod_group,
         header = i18n.mod_table.mod_groups,
         fields = {'mods.mod_group'},
         fields = {'mods.mod_groups'},
         display = function(tpl_args, frame, tr, data)
         display = function(tpl_args, tr, data, fields)
             tr
             local k = 'mods.mod_groups'
                :tag('td')
            data[k] = table.concat(m_util.string.split(data[k], ',%s*'), ', ')
                    :wikitext(data['mods.mod_group'])
            h.tbl.display.factory.value{}(tpl_args, tr, data, fields)
         end,
         end,
         order = 2002,
         order = 2002,
Line 253: Line 171:
         header = i18n.mod_table.mod_type,
         header = i18n.mod_table.mod_type,
         fields = {'mods.mod_type'},
         fields = {'mods.mod_type'},
         display = function(tpl_args, frame, tr, data)
         display = h.tbl.display.factory.value{},
            tr
                :tag('td')
                    :wikitext(data['mods.mod_type'])
        end,
         order = 2003,
         order = 2003,
         sort_type = 'text',
         sort_type = 'text',
Line 265: Line 179:
         header = i18n.mod_table.required_level,
         header = i18n.mod_table.required_level,
         fields = {'mods.required_level'},
         fields = {'mods.required_level'},
         display = function(tpl_args, frame, tr, data)
         display = h.tbl.display.factory.value{},
            tr
                :tag('td')
                    :wikitext(data['mods.required_level'])
        end,
         order = 2004,
         order = 2004,
     },
     },
Line 276: Line 186:
         header = i18n.mod_table.labyrinth,
         header = i18n.mod_table.labyrinth,
         fields = {string.format([[
         fields = {string.format([[
CONCAT(
                CONCAT(
    CASE mods.required_level  
                    CASE mods.required_level
        WHEN 32 THEN "%s"  
                        WHEN 32 THEN "%s"
        WHEN 53 THEN "%s"  
                        WHEN 53 THEN "%s"
        WHEN 66 THEN "%s"  
                        WHEN 66 THEN "%s"
        WHEN 75 THEN "%s"  
                        WHEN 75 THEN "%s"
    END
                        WHEN 83 THEN "%s"
)=labyrinth_text
                    END
            ]], i18n.mod_table.normal_labyrinth, i18n.mod_table.cruel_labyrinth, i18n.mod_table.merciless_labyrinth, i18n.mod_table.eternal_labyrinth)},
                )=labyrinth_text]],
         display = function(tpl_args, frame, tr, data)
            i18n.mod_table.normal_labyrinth,
            tr
            i18n.mod_table.cruel_labyrinth,
                :tag('td')
            i18n.mod_table.merciless_labyrinth,
                    :wikitext(data['labyrinth_text'])
            i18n.mod_table.eternal_labyrinth,
        end,
            i18n.mod_table.belt_labyrinth)
        },
         display = h.tbl.display.factory.value{},
         order = 2005,
         order = 2005,
         sort_type = 'text',
         sort_type = 'text',
Line 297: Line 209:
         header = i18n.mod_table.stat_text,
         header = i18n.mod_table.stat_text,
         fields = {'mods.stat_text'},
         fields = {'mods.stat_text'},
         display = function(tpl_args, frame, tr, data)
         display = function(tpl_args, tr, data, fields)
             local text
             local value
             -- map display type shows this in another column, remove this text to avoid clogging up the list
             -- map display type shows this in another column, remove this text to avoid clogging up the list
             if tpl_args.type == 'map' then
             if tpl_args.type == 'map' then
                 local texts = m_util.string.split(data['mods.stat_text'], '<br>')
                 local texts = m_util.string.split(data['mods.stat_text'], '<br>')
                 local out = {}
                 local out = {}
               
 
                 local valid
                 local valid
                 for _, v in ipairs(texts) do
                 for _, v in ipairs(texts) do
Line 313: Line 225:
                         end
                         end
                     end
                     end
                   
 
                     if valid then
                     if valid then
                         table.insert(out, v)
                         table.insert(out, v)
                     end
                     end
                 end
                 end
               
 
                 text = table.concat(out, '<br>')
                 value = table.concat(out, '<br>')
             else
             else
                 text = data['mods.stat_text']
                 value = data['mods.stat_text']
             end
             end
              
             data['mods.stat_text'] = value
             tr
             h.tbl.display.factory.value{color='mod'}(tpl_args, tr, data, fields)
                :tag('td')
                    :wikitext(text)
         end,
         end,
         order = 3000,
         order = 3000,
         sort_type = 'text',
         sort_type = 'text',
     },
     },
     {
     {
         arg = 'buff',
         arg = 'buff',
         header = i18n.mod_table.buff,
         header = i18n.mod_table.buff,
         fields = {'mods.granted_buff_id', 'mods.granted_buff_value'},
         fields = {'mods.granted_buff_id', 'mods.granted_buff_value'},
         display = function(tpl_args, frame, tr, data)
         display = h.tbl.display.factory.value{delimiter=' '},
            tr
                :tag('td')
                    :wikitext(string.format('%s %s', data['mods.granted_buff_id'], data['mods.granted_buff_value']))
        end,
         order = 4000,
         order = 4000,
         sort_type = 'text',
         sort_type = 'text',
Line 348: Line 253:
         header = i18n.mod_table.granted_skill,
         header = i18n.mod_table.granted_skill,
         fields = {'mods.granted_skill'},
         fields = {'mods.granted_skill'},
         display = function(tpl_args, frame, tr, data)
         display = h.tbl.display.factory.value{},
            tr
                :tag('td')
                    :wikitext(data['mods.granted_skill'])
        end,
         order = 4001,
         order = 4001,
         sort_type = 'text',
         sort_type = 'text',
Line 360: Line 261:
         header = i18n.mod_table.tags,
         header = i18n.mod_table.tags,
         fields = {'mods.tags'},
         fields = {'mods.tags'},
         display = function(tpl_args, frame, tr, data)
         display = function(tpl_args, tr, data, fields)
             tr
             local k = 'mods.tags'
                :tag('td')
            data[k] = table.concat(m_util.string.split(data[k], ',%s*'), ', ')
                    :wikitext(table.concat(m_util.string.split(data['mods.tags'], ','), ', '))
            h.tbl.display.factory.value{}(tpl_args, tr, data, fields)
         end,
         end,
         order = 5000,
         order = 5000,
Line 385: Line 286:
mod_table.weights = {'spawn_weights', 'generation_weights'}
mod_table.weights = {'spawn_weights', 'generation_weights'}


function p.mod_table(frame)
-- ----------------------------------------------------------------------------
-- Main functions
-- ----------------------------------------------------------------------------
 
local function _mod_table(tpl_args)
     --[[
     --[[
     Creates a generic table for modifiers.
     Creates a generic table for modifiers.
   
 
     Examples
     Examples
     --------
     --------
     = p.mod_table{
     = p.mod_table{
         q_tables='spawn_weights',
         q_tables='mod_spawn_weights',
         q_join='mods._pageID=spawn_weights._pageID',
         q_join='mods._pageID=mod_spawn_weights._pageID',
         q_where='mods.generation_type = 10 AND spawn_weights.tag = "boots" AND spawn_weights.weight > 0',
         q_where='mods.generation_type = 10 AND mod_spawn_weights.tag = "boots" AND mod_spawn_weights.weight > 0',
         q_orderBy='mods.id, mods.required_level',
         q_orderBy='mods.id, mods.required_level',
         q_limit=100,
         q_limit=100,
Line 400: Line 305:
         enchantment=1,
         enchantment=1,
     }
     }
   
     ]]
     ]]
   
 
   
    tpl_args = getArgs(frame, {
        parentFirst = true
    })
    frame = m_util.misc.get_frame(frame)
   
     -- default to enabled
     -- default to enabled
     tpl_args.name = tpl_args.name or true
     tpl_args.name = tpl_args.name or true
   
 
     for _, key in ipairs(mod_table.weights) do
     for _, key in ipairs(mod_table.weights) do
         tpl_args[key] = m_util.cast.boolean(tpl_args[key])
         tpl_args[key] = m_util.cast.boolean(tpl_args[key])
     end
     end
   
 
     if string.find(tpl_args.q_where, '%[%[') ~= nil then
     if string.find(tpl_args.q_where, '%[%[') ~= nil then
         error('SMW leftover in where clause')
         error('SMW leftover in where clause')
     end
     end
   
 
     local row_infos = {}
     local row_infos = {}
     for _, row_info in ipairs(mod_table.data) do
     for _, row_info in ipairs(mod_table.data) do
Line 427: Line 325:
         elseif type(row_info.arg) == 'string' and m_util.cast.boolean(tpl_args[row_info.arg]) then
         elseif type(row_info.arg) == 'string' and m_util.cast.boolean(tpl_args[row_info.arg]) then
             enabled = true
             enabled = true
         elseif type(row_info.arg) == 'table' then  
         elseif type(row_info.arg) == 'table' then
             for _, argument in ipairs(row_info.arg) do
             for _, argument in ipairs(row_info.arg) do
                 if m_util.cast.boolean(tpl_args[argument]) then
                 if m_util.cast.boolean(tpl_args[argument]) then
Line 435: Line 333:
             end
             end
         end
         end
       
 
         if enabled then
         if enabled then
             row_info.options = row_info.options or {}
             row_info.options = row_info.options or {}
Line 441: Line 339:
         end
         end
     end
     end
   
 
     -- sort the rows
     -- sort the rows
     table.sort(row_infos, function (a, b)
     table.sort(row_infos, function (a, b)
         return (a.order or 0) < (b.order or 0)
         return (a.order or 0) < (b.order or 0)
     end)
     end)
   
 
     -- Set tables
     -- Set required and extra tables:
     local tables = 'mods'  
     local tables = {'mods'}
     if tpl_args.q_tables then
     for _, v in ipairs(m_util.string.split_args(tpl_args.q_tables, {',%s*'})) do
         tables = tables .. ',' .. tpl_args.q_tables
         tables[#tables+1] = v
     end
     end
   
 
   
     -- Set required and extra fields:
     -- Set required fields
     local fields = {
     local fields = {
         'mods._pageID',
         'mods._pageID',
     }
     }
     for _, rowinfo in ipairs(row_infos) do
     for _, row_info in ipairs(row_infos) do
         if type(rowinfo.fields) == 'function' then
         if type(row_info.fields) == 'function' then
             rowinfo.fields = rowinfo.fields()
             row_info.fields = row_info.fields()
         end
         end
         for index, field in ipairs(rowinfo.fields) do
         for index, field in ipairs(row_info.fields) do
             rowinfo.options[index] = rowinfo.options[index] or {}
             row_info.options[index] = row_info.options[index] or {}
             fields[#fields+1] = field
             fields[#fields+1] = field
         end
         end
     end
     end
      
     tpl_args._extra_fields = m_util.string.split_args(tpl_args.q_fields, {',%s*'})
     -- Parse query arguments
    for _, v in ipairs(tpl_args._extra_fields) do
        fields[#fields+1] = v
    end
 
     -- Parse query arguments:
     local query = {
     local query = {
         -- Workaround: fix duplicates
         -- Workaround: fix duplicates
         groupBy='mods._pageID',
         groupBy='mods._pageID',
     }
     }
     for key, value in pairs(tpl_args) do  
     for key, value in pairs(tpl_args) do
         if string.sub(key, 0, 2) == 'q_' then
         if string.sub(key, 0, 2) == 'q_' then
             query[string.sub(key, 3)] = value
             query[string.sub(key, 3)] = value
         end
         end
     end
     end
   
 
   
     local results = m_cargo.query(tables, fields, query)
    fields = table.concat(fields, ',')
 
    if tpl_args.q_fields then
        fields = fields .. ' ,' .. tpl_args.q_fields
    end
   
     local results = cargo.query(
        tables,
        fields,
        query
    )
   
     if #results == 0 then
     if #results == 0 then
         if tpl_args.default ~= nil then
         if tpl_args.default ~= nil then
Line 498: Line 389:
         end
         end
     end
     end
   
 
     -- this might be needed in other queries, currently not checking if it's actually needed
     -- this might be needed in other queries, currently not checking if
    -- because performance impact should be neglible
    -- it's actually needed because performance impact should be neglible
     local page_ids = {}
     local page_ids = {}
     for _, row in ipairs(results) do
     for _, row in ipairs(results) do
         page_ids[#page_ids+1] = string.format('mods._pageID="%s"', row['mods._pageID'])
         page_ids[#page_ids+1] = string.format(
            'mods._pageID="%s"',
            row['mods._pageID']
        )
     end
     end
     page_ids = table.concat(page_ids, ' OR ')
     page_ids = table.concat(page_ids, ' OR ')
   
 
     local weights = {}
     local weights = {}
     for _, key in ipairs(mod_table.weights) do
     for _, key in ipairs(mod_table.weights) do
         if tpl_args[key] then
         if tpl_args[key] then
             weights[key] = h.query_weights(key, page_ids)
             weights[key] = h.query_weights('mod_' .. key, page_ids)
         end
         end
     end
     end
   
 
     local stats
     local stats
     if tpl_args.type == 'map' then
     if tpl_args.type == 'map' then
Line 520: Line 414:
             query_stat_ids[#query_stat_ids+1] = string.format('mod_stats.id="%s"', k)
             query_stat_ids[#query_stat_ids+1] = string.format('mod_stats.id="%s"', k)
         end
         end
          
 
        local stat_results = cargo.query(
         stats = m_cargo.map_results_to_id{
            'mods,mod_stats',
            results=m_cargo.query(
            'mods._pageID,mod_stats.id,mod_stats.min,mod_stats.max',
                {'mods', 'mod_stats'},
            {
                {
                where=string.format('(%s) AND (%s)', page_ids, table.concat(query_stat_ids, ' OR ')),
                    'mods._pageID',
                join='mods._pageID=mod_stats._pageID',
                    'mod_stats.id',
                orderBy='mods.id ASC',
                    'mod_stats.min',
                 limit=5000,
                    'mod_stats.max',
             }
                },
        )
                {
        if #stat_results == 5000 then
                    where=string.format(
             error('Hit maximum cargo results')
                        '(%s) AND (%s)',
        end
                        page_ids,
       
                        table.concat(query_stat_ids, ' OR ')
        stats = m_util.cargo.map_results_to_id{results=stat_results, field='mods._pageID'}
                    ),
                    join='mods._pageID=mod_stats._pageID',
                    orderBy='mods.id ASC',
                 }
             ),
             field='mods._pageID'
        }
 
         -- In addition map stats to stat <-> min/max pairs
         -- In addition map stats to stat <-> min/max pairs
         for page_id, rows in pairs(stats) do
         for page_id, rows in pairs(stats) do
             local stat_id_map = {}
             local stat_id_map = {}
             for _, row in ipairs(rows) do
             for _, row in ipairs(rows) do
                 stat_id_map[row['mod_stats.id']] = {min=tonumber(row['mod_stats.min']), max=tonumber(row['mod_stats.max'])}
                 stat_id_map[row['mod_stats.id']] = {
                    min=tonumber(row['mod_stats.min']),
                    max=tonumber(row['mod_stats.max'])
                }
             end
             end
             stats[page_id] = stat_id_map
             stats[page_id] = stat_id_map
         end
         end
     end
     end
   
 
     --
     --
     -- Display
     -- Display
     --
     --
   
 
     -- Preformance optimization
     -- Preformance optimization
     if tpl_args.q_fields then
     for index, field in ipairs(tpl_args._extra_fields) do
        tpl_args._extra_fields = m_util.string.split(tpl_args.q_fields, ',')
        field = m_util.string.split(field, '%s*=%s*')
        for index, field in ipairs(tpl_args._extra_fields) do
        -- field[2] will be nil if there is no alias
            field = m_util.string.split(field, '=')
        tpl_args._extra_fields[index] = field[2] or field[1]
            -- field[2] will be nil if there is no alias
            tpl_args._extra_fields[index] = field[2] or field[1]
        end
    else
        tpl_args._extra_fields = {}
     end
     end
   
 
     local tbl = mw.html.create('table')
     local tbl = mw.html.create('table')
     tbl:attr('class', 'wikitable sortable modifier-table')
     tbl:attr('class', 'wikitable sortable modifier-table')
     -- Header
     -- Header
   
 
     local tr = tbl:tag('tr')
     local tr = tbl:tag('tr')
     for _, row_info in ipairs(row_infos) do
    local display_fields = {}
     for i, row_info in ipairs(row_infos) do
        for j, field in ipairs(row_info.fields) do
            -- Aliased name is used as keys in the results:
            field = m_util.string.split(field, '%s*=%s*')
            field = field[2] or field[1]
 
            -- Make a new field table since mod_table.data will remain
            -- modified the 2nd time a function is run and then crash.
            if j == 1 then
                display_fields[i] = {}
            end
            display_fields[i][j] = field
        end
 
         tr
         tr
             :tag('th')
             :tag('th')
Line 574: Line 487:
                 :done()
                 :done()
     end
     end
   
 
     if tpl_args.type == 'map' then
     if tpl_args.type == 'map' then
         for stat_id, data in pairs(mod_table.stat_ids) do
         for stat_id, data in pairs(mod_table.stat_ids) do
Line 584: Line 497:
         end
         end
     end
     end
   
 
     for _, key in ipairs(mod_table.weights) do
     for _, key in ipairs(mod_table.weights) do
         if tpl_args[key] then
         if tpl_args[key] then
Line 592: Line 505:
         end
         end
     end
     end
   
 
     for _, field in ipairs(tpl_args._extra_fields) do
     for _, field in ipairs(tpl_args._extra_fields) do
         tr
         tr
Line 598: Line 511:
                 :wikitext(field)
                 :wikitext(field)
     end
     end
   
 
     -- Body
     -- Body
   
 
     for _, row in ipairs(results) do
     for _, row in ipairs(results) do
         tr = tbl:tag('tr')
         tr = tbl:tag('tr')
               
 
         for _, rowinfo in ipairs(row_infos) do
         for i, row_info in ipairs(row_infos) do
             -- this has been cast from a function in an earlier step
             -- this has been cast from a function in an earlier step
             local display = true
             local display = true
             for index, field in ipairs(rowinfo.fields) do
             for j, field in ipairs(display_fields[i]) do
                 -- this will bet set to an empty value not nil confusingly
                 -- this will bet set to an empty value not nil confusingly
                 if row[field] == '' then
                 if row[field] == nil or row[field] == '' then
                     if rowinfo.options[index].optional ~= true then
                     if row_info.options[j].optional ~= true then
                         display = false
                         display = false
                         break
                         break
Line 619: Line 532:
             end
             end
             if display then
             if display then
                 rowinfo.display(tpl_args, frame, tr, row, rowinfo.fields)
                 row_info.display(tpl_args, tr, row, display_fields[i])
             else
             else
                 tr:wikitext(m_util.html.td.na())
                 tr:node(m_util.html.table_cell('na'))
             end
             end
         end
         end
       
 
         if tpl_args.type == 'map' then
         if tpl_args.type == 'map' then
             for stat_id, data in pairs(mod_table.stat_ids) do
             for stat_id, data in pairs(mod_table.stat_ids) do
Line 642: Line 555:
                             :done()
                             :done()
                 else
                 else
                     tr:wikitext(m_util.html.td.na())
                     tr:node(m_util.html.table_cell('na'))
                 end
                 end
             end
             end
         end
         end
       
 
         for _, key in ipairs(mod_table.weights) do
         for _, key in ipairs(mod_table.weights) do
             if tpl_args[key] then
             if tpl_args[key] then
                 local weight_out = {}
                 local weight_out = {}
                local fields = {
                    tag = string.format('mod_%s.tag', key),
                    value = string.format('mod_%s.value', key),
                }
                 for _, wrow in ipairs(weights[key][row['mods._pageID']]) do
                 for _, wrow in ipairs(weights[key][row['mods._pageID']]) do
                     weight_out[#weight_out+1] = string.format('%s %s', wrow[key .. '.tag'], wrow[key .. '.weight'])
                     if wrow[fields.tag] and wrow[fields.value] then
                        weight_out[#weight_out+1] = string.format(
                            '%s %s',
                            wrow[fields.tag],
                            wrow[fields.value]
                        )
                    end
                 end
                 end
                 if #weight_out > 0 then
                 if #weight_out > 0 then
                     tr
                     tr
                            :tag('td')
                        :tag('td')
                                :wikitext(table.concat(weight_out, '<br>'))
                            :wikitext(table.concat(weight_out, '<br>'))
                                :done()
                            :done()
                 else  
                 else
                     tr:wikitext(m_util.html.td.na())
                     tr:node(m_util.html.table_cell('na'))
                 end
                 end
             end
             end
         end
         end
       
 
         for _, field in ipairs(tpl_args._extra_fields) do
         for _, field in ipairs(tpl_args._extra_fields) do
             if row[field] then
             if row[field] then
Line 670: Line 594:
                         :wikitext(row[field])
                         :wikitext(row[field])
             else
             else
                 tr:wikitext(m_util.html.td.na())
                 tr:node(m_util.html.table_cell('na'))
             end
             end
         end
         end
     end
     end
   
 
     return tostring(tbl)
     return tostring(tbl)
end
end


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


local p = {}


--
-- Template:Modifier table
--
p.mod_table = m_util.misc.invoker_factory(_mod_table, {
    parentFirst = true,
})


-- ---------------------------------------------------------------------
--  
-- Modifier link
-- Debug
-- ---------------------------------------------------------------------
--  
 
 
function p.modifier_link(frame)
    --[[
    Finds and links to a modifier in formatted form.
   
    To do list
    ----------
    * Standardize hoverbox so it can be used in multiple places easily
    and make it easy to add rows of data.
   
    Examples
    --------
    = p.modifier_link{"Tyrannical"}
    = p.modifier_link{"Flaring"}
    = p.modifier_link{"Dictator's"}
    = p.modifier_link{"StrDexMaster%"}
    = p.modifier_link{"LocalIncreasedPhysicalDamagePercentAndAccuracyRating8", display='max', statid='local_physical_damage_+%'}
   
    ]]
 
    -- Get template args:
    local tpl_args = getArgs(frame, {parentFirst = true})
    local frame = m_util.misc.get_frame(frame)
       
    -- Aliases:
    tpl_args.modid = tpl_args.modid or tpl_args.id or tpl_args[1] or ''
   
    -- Define query arguments:
    local tables = {'mods', 'mod_stats', 'spawn_weights'}
    local fields = {'mods.name', 'mods.stat_text', 'mods.stat_text_raw', 'mods.generation_type', 'mods.tags', 'mods._pageName', 'mod_stats.max', 'mod_stats.min', 'spawn_weights.tag', 'spawn_weights.weight', 'mods.id', 'mod_stats.id'}
    local query = {
        join = 'mods._pageName=mod_stats._pageName, mods._pageName=spawn_weights._pageName',
        where = string.format(
            '(mods.name LIKE "%s" or mods.id LIKE "%s" or mods.stat_text LIKE "%s" or mods.stat_text_raw LIKE "%s") AND mod_stats.id LIKE "%%%s%%"',
            tpl_args.modid,
            tpl_args.modid,
            tpl_args.modid,
            tpl_args.modid,
            tpl_args.statid or '%'
        ),
        -- groupBy = 'mods._pageID, mod_stats.id, spawn_weights.tag',
    }
   
    -- Query cargo rows:
    local results = m_util.cargo.query(tables, fields, query, args)
   
    -- Create own list for each cargo table and group by page name:
    tpl_args.tbl = {}
    for _,v in ipairs(tables) do
        tpl_args.tbl[v] = {}
    end
   
    tpl_args.results_unique = {}
    local hash = {}
    for _,v in ipairs(results) do
        for ii, vv in pairs(tpl_args.tbl) do
            if tpl_args.tbl[ii][v['mods._pageName']] == nil then
                tpl_args.tbl[ii][v['mods._pageName']] = {}
            end
            local n = #tpl_args.tbl[ii][v['mods._pageName']] or 0
            tpl_args.tbl[ii][v['mods._pageName']][n+1] = v
           
            -- Get a sorted list that only has unique page names:
            if hash[v['mods._pageName']] ~= true then
                local m = #tpl_args.results_unique
                tpl_args.results_unique[m+1] = v
                hash[v['mods._pageName']] = true
            end
        end
    end
 
    -- If no results are found then just create a empty list to avoid
    -- index errors.
    if tpl_args.results_unique[1] == nil then
        tpl_args.results_unique[1] = {}
    end
   
    -- Helpful error handling:
    local err_tbl = {
        {
            bool = #results == 0,
            disp = {
                i18n.errors.no_results,
            }
        },
        {
            bool = #tpl_args.results_unique > 1,
            disp = {
                i18n.errors.multiple_results,
                h.disambiguate_mod_name(tpl_args.results_unique),
            },
        },
        {
            bool = tpl_args.modid ~= tpl_args.results_unique[1]['mods.id'],
            disp = {
                string.gsub(
                    i18n.errors.incorrect_modid,
                    '%%s',
                    tpl_args.modid,
                    1
                ),
                h.disambiguate_mod_name(tpl_args.results_unique),
            },
        },
    }
    for _,v in ipairs(err_tbl) do
        if v.bool then
            local cats = {'Pages with modifier link errors'}
            return m_util.html.error(
                {msg = string.format(v.disp[1], v.disp[2]) .. m_util.misc.add_category(cats)}
            )
        end
    end
   
    -- Display formats:
    local display = {
        abbr = {
            display = function(tpl_args, frame)
                local name = m_util.html.poe_color(
                    'mod',
                    string.format(
                        '[[%s|%s]]',
                        tpl_args.results_unique[1]['mods._pageName'],
                        tpl_args.results_unique[1]['mods.name'] or tpl_args.results_unique[1]['mods.id']
                    )
                )
               
                local tooltip_table = {
                    m_util.html.poe_color(
                        'mod',
                        tpl_args.results_unique[1]['mods.name'] or tpl_args.results_unique[1]['mods.id']
                    ),
                    m_util.html.poe_color(
                        'help',
                        m_game.constants.mod.generation_types[
                            tonumber(
                                tpl_args.results_unique[1]['mods.generation_type']
                            )
                        ].full
                    ),
                    -- m_util.html.poe_color(
                        -- 'help',
                      -- table.concat(
                            -- m_util.string.split(tpl_args.results_unique[1]['mods.tags'] or '', ','),
                            -- ', '
                        -- )
                    -- ),
                    m_util.html.poe_color(
                        'normal',
                        tpl_args.results_unique[1]['mods.stat_text_raw']
                    ),
                }
               
                local tt_tbl_fltrd = {}
                for _,v in ipairs(tooltip_table) do
                    if v ~= nil and v ~= '' then
                        tt_tbl_fltrd[#tt_tbl_fltrd+1] = v
                    end
                end
                local tooltip = table.concat(tt_tbl_fltrd, '<br>')
               
                return m_util.html.tooltip(name, tooltip, class)
            end,
        },
        verbose = {
            display = function(tpl_args, frame)
                return string.format(
                    '%s - %s (%s)',
                    m_util.html.poe_color(
                        'mod',
                        string.format(
                            '[[%s|%s]]',
                            tpl_args.results_unique[1]['mods._pageName'],
                            tpl_args.results_unique[1]['mods.name'] or tpl_args.results_unique[1]['mods.id']
                        )
                    ),
                    m_util.html.poe_color(
                        'mod',
                        string.gsub(
                            tpl_args.results_unique[1]['mods.stat_text'],
                            '<br>',
                            ', '
                        )
                    ),
                    m_game.constants.mod.generation_types[
                        tonumber(
                            tpl_args.results_unique[1]['mods.generation_type']
                        )
                    ].full
                )
            end,
        },
        stat_text = {
            display = function(tpl_args, frame)
                return m_util.html.poe_color(
                    'mod',
                    tpl_args.results_unique[1]['mods.stat_text']
                )
            end,
        },
        max = {
            display = function(tpl_args, frame)
                local statid = {}
                for _,v in ipairs(tpl_args.tbl.mod_stats[tpl_args.results_unique[1]['mods._pageName']]) do
                    statid[#statid+1] = v['mod_stats.id']
                    if tpl_args.statid == v['mod_stats.id'] then
                        return v['mod_stats.max']
                    end
                end
               
                if tpl_args.statid == nil then
                    return m_util.html.error(
                        {
                            msg = string.format(
                                i18n.errors.undefined_statid,
                                table.concat(statid, ', ')
                            )
                        }
                    )
                end
            end
        },
        min = {
            display = function(tpl_args, frame)
                local statid = {}
                for _,v in ipairs(tpl_args.tbl.mod_stats[tpl_args.results_unique[1]['mods._pageName']]) do
                    statid[#statid+1] = v['mod_stats.id']
                    if tpl_args.statid == v['mod_stats.id'] then
                        return v['mod_stats.min']
                    end
                end
               
                if tpl_args.statid == nil then
                    return m_util.html.error(
                        {
                            msg = string.format(
                                i18n.errors.undefined_statid,
                                table.concat(statid, ', ')
                            )
                        }
                    )
                end
            end
        },
    }
   
    return display[tpl_args.display or 'abbr'].display(tpl_args, frame)
end
 
 
 
 
-- ---------------------------------------------------------------------
-- Drop down list
-- ---------------------------------------------------------------------
 
function h.get_spawn_chance(frame)
    --[[
    Calculates the spawn chance of a set of mods that all have a
    spawn weight.
   
    ]]
   
    -- Get template arguments:
    local tpl_args = getArgs(frame, {parentFirst=true})
    local frame = m_util.misc.get_frame(frame)
   
    -- Get the table:
    local tbl = tpl_args['tbl']
   
    -- Probabilities affecting the result besides the spawn weight:
    local chance_multiplier = tonumber(tpl_args['chance_multiplier']) or 1
   
    -- Total number of outcomes.
    local N = 0
    for i,_ in ipairs(tbl) do
        N = N + tbl[i]['spawn_weights.weight']
    end
   
    for i,_ in ipairs(tbl) do
        -- Number of ways it can happen:
        local n = tbl[i]['spawn_weights.weight']
 
        -- Truncated value:
        tbl[i]['spawn_weights.chance'] = string.format(
            "%0.2f%%",
            n/N * chance_multiplier*100
        )
    end
   
    return tbl
end
   
function p.drop_down_table(frame)
    --[[
    This function queries mods that can spawn on an item. It compares
    the item tags and the spawn weight tags. If there's a match and
    the spawn weight is larger than zero, then that mod is added to a
    drop down list.
   
    To Do:
    * Add support to:
        * Forsaken masters
        * Bestiary
    * Add a proper expand/collapse toggle for the entire header row so
        it reacts together with mw-collapsible.
    * Show Mod group in a better way perhaps:
        Mod group [Collapsible, default=Expanded]
            # to Damage [Collapsible, default=Collapsed]
                3 to Damage
                5 to Damage
    * Add a where condition that somehow filters out mods that obviously
      wont match with the item. spawn_weights.weight>0 isn't enough due
      to possible edge cases.
       
   
    Examples:
    Weapons
    = p.drop_down_table{item = 'Rusted Hatchet', header = 'One Handed Axes'}
    = p.drop_down_table{item = 'Stone Axe', header = 'Two Handed Axes'}
 
    Accessories
    = p.drop_down_table{item = 'Amber Amulet', header = 'Amulets'}
   
    Jewels
    = p.drop_down_table{item = 'Cobalt Jewel', header = 'Jewels'}
   
    Armour
    = p.drop_down_table{item = 'Plate Vest', header = 'Body armours'}
   
    Boots
    = p.drop_down_table{item = 'Iron Greaves', header = 'Boots'}
   
    = p.drop_down_table{
        item = 'Fishing Rod',
        header = 'FISH PLEASE',
        item_tags = 'fishing_rod',
        extra_fields = 'mods.tags'
    }
   
    = p.drop_down_table{
        item = 'Fishing Rod',
        item_tags = 'axe, one_hand_weapon, onehand, weapon, default',
        extra_item_tags = 'fishing_rod'
    }
   
    = p.drop_down_table{
        item = 'Vaal Blade',
    }
       
    ]]
 
    -- Get template arguments:
    local tpl_args = getArgs(frame, {parentFirst=true})
    local frame = m_util.misc.get_frame(frame)
   
    -- Format the cargo query:
    local where
    for _,v in ipairs({
            {tpl_args.page, 'items._pageName = "%s"'},
            {tpl_args.item, 'items.name = "%s"'},
    }) do
        if v[1] ~= nil then
            where = string.format(v[2], v[1])
            break
        end
    end
   
    local item_info = m_cargo.query(
        {'items'},
        {'items.name', 'items.tags', 'items.class', 'items.class_id', 'items.inventory_icon', 'items.html', 'items.release_version', 'items._pageName'},
        {
            where=where,
            groupBy='items._pageName',
            orderBy = 'items.name, items.release_version DESC',
        }
    )[1]
   
    -- Set the item class as key and the corresponding mod domain as
    -- value:
    local class_to_domain = {
        ['Life Flasks']=2,
        ['Mana Flasks']=2,
        ['Hybrid Flasks']=2,
        ['Utility Flasks']=2,
        ['Critical Utility Flasks']=2,
        ['Maps']=5,
        ['Jewel']=10,
        ['Leaguestones']=12,
        ['Abyss Jewel']=13,
    }
    -- Get the domain, if it's not defined in the table assume it's
    -- in the item domain.
    item_info['items.domain'] = class_to_domain[item_info['items.class']] or 1
   
    -- Convert the mod domain number to understandable text:
    item_info['items.domain_text'] = m_game.constants.mod.domains[item_info['items.domain']]['short_lower']
   
    -- For some reason cargo queried item tags, are not comma-space
    -- separated any more.
    if tpl_args.item_tags ~= nil then
        tpl_args.item_tags = m_util.string.split(tpl_args.item_tags, ', ')
    else
        tpl_args.item_tags = m_util.string.split(item_info['items.tags'], ',')
    end
    if tpl_args.extra_item_tags then
        local extra_item_tags = m_util.string.split(tpl_args.extra_item_tags, ', ')
        for _,v in ipairs(extra_item_tags) do
            tpl_args.item_tags[#tpl_args.item_tags+1] = v
        end
    end
   
    -- Create drop down lists in these sections and query in these
    -- generation types.
   
    local elder_tag = m_game.constants.item.classes[item_info['items.class_id']]['elder_tag']
    local shaper_tag = m_game.constants.item.classes[item_info['items.class_id']]['shaper_tag']
   
    local section = {}
    section = {
        {
            header = i18n.drop_down_table.prefix,
            where = function(tpl_args, frame, value)
                local where = {}
                for _, item_tag in ipairs(tpl_args.item_tags) do
                    where[#where+1] = string.format(
                        '(spawn_weights.tag="%s" AND mods.generation_type=%s AND mods.domain=%s AND mods.stat_text IS NOT NULL)',
                        item_tag,
                        1,
                        item_info['items.domain']
                    )
                end
               
                return table.concat(where, ' OR ')
            end,
        },
        {
            header = i18n.drop_down_table.suffix,
            where = function(tpl_args, frame, value)
                local where = {}
                for _, item_tag in ipairs(tpl_args.item_tags) do
                    where[#where+1] = string.format(
                        '(spawn_weights.tag="%s" AND mods.generation_type=%s AND mods.domain=%s AND mods.stat_text IS NOT NULL)',
                        item_tag,
                        2,
                        item_info['items.domain']
                    )
                end
               
                return table.concat(where, ' OR ')
            end,
        },
        {
            tags = {elder_tag},
            header = i18n.drop_down_table.elder_prefix,
            where = function(tpl_args, frame, value)
                return string.format(
                    '(spawn_weights.tag="%s" AND mods.generation_type=%s AND mods.domain=%s AND mods.stat_text IS NOT NULL)',
                    elder_tag,
                    1,
                    item_info['items.domain']
                )
            end,
        },
        {
            tags = {elder_tag},
            header = i18n.drop_down_table.elder_suffix,
            where = function(tpl_args, frame, value)
                return string.format(
                    '(spawn_weights.tag="%s" AND mods.generation_type=%s AND mods.domain=%s AND mods.stat_text IS NOT NULL)',
                    elder_tag,
                    2,
                    item_info['items.domain']
                )
            end,
        },
        {
            tags = {shaper_tag},
            header = i18n.drop_down_table.shaper_prefix,
            where = function(tpl_args, frame, value)
                return string.format(
                    '(spawn_weights.tag="%s" AND mods.generation_type=%s AND mods.domain=%s AND mods.stat_text IS NOT NULL)',
                    shaper_tag,
                    1,
                    item_info['items.domain']
                )
            end,
        },
        {
            tags = {shaper_tag},
            header = i18n.drop_down_table.shaper_suffix,
            where = function(tpl_args, frame, value)
                return string.format(
                    '(spawn_weights.tag="%s" AND mods.generation_type=%s AND mods.domain=%s AND mods.stat_text IS NOT NULL)',
                    shaper_tag,
                    2,
                    item_info['items.domain']
                )
            end,
        },
        {
            header = i18n.drop_down_table.delve_prefix,
            where = function(tpl_args, frame, value)
                local where = {}
                for _, item_tag in ipairs(tpl_args.item_tags) do
                    where[#where+1] = string.format(
                        '(spawn_weights.tag="%s" AND mods.generation_type=%s AND mods.domain=%s AND mods.stat_text IS NOT NULL)',
                        item_tag,
                        1,
                        16
                    )
                end
               
                return table.concat(where, ' OR ')
            end,
        },
        {
            header = i18n.drop_down_table.delve_suffix,
            where = function(tpl_args, frame, value)
                local where = {}
                for _, item_tag in ipairs(tpl_args.item_tags) do
                    where[#where+1] = string.format(
                        '(spawn_weights.tag="%s" AND mods.generation_type=%s AND mods.domain=%s AND mods.stat_text IS NOT NULL)',
                        item_tag,
                        2,
                        16
                    )
                end
               
                return table.concat(where, ' OR ')
            end,
        },
        {
            header = i18n.drop_down_table.corrupted,
            where = function(tpl_args, frame, value)
                local where = {}
                for _, item_tag in ipairs(tpl_args.item_tags) do
                    where[#where+1] = string.format(
                        '(spawn_weights.tag="%s" AND mods.generation_type=%s AND mods.domain=%s AND mods.stat_text IS NOT NULL)',
                        item_tag,
                        5,
                        item_info['items.domain']
                    )
                end
               
                return table.concat(where, ' OR ')
            end,
            chance_multiplier = 1/4, -- See Vaal orb, for the 4 possible events.
            is_implicit = true,
        },
        {
            header = i18n.drop_down_table.enchant,
            where = function(tpl_args, frame, value)
                local where = {}
                for _, item_tag in ipairs(tpl_args.item_tags) do
                    where[#where+1] = string.format(
                        '(spawn_weights.tag="%s" AND mods.generation_type=%s AND mods.domain=%s AND mods.stat_text IS NOT NULL)',
                        item_tag,
                        10,
                        item_info['items.domain']
                    )
                end
               
                return table.concat(where, ' OR ')
            end,
            is_implicit = true,
        },
    }
     
     
    -- Introductory text:
    local out = {}
    out[#out+1] = string.format(
        '==%s== \n',
        tpl_args['header'] or table.concat(tpl_args.item_tags, ', ')
    )
    out[#out+1] = string.format(
        '<div style="float: right; text-align:center"><div class="mw-collapsible-collapse-all" style="cursor:pointer;">[%s]</div><hr><div class="mw-collapsible-expand-all" style="cursor:pointer;">[%s]</div></div>',
        i18n.drop_down_table.collapse_all,
        i18n.drop_down_table.expand_all
    )
    out[#out+1] = string.format('%s %s.<br><br><br>',
        i18n.drop_down_table.table_intro,
        f_item_link{
            page=item_info['items._pageName'],
            name=item_info['items.name'],
            inventory_icon=item_info['items.inventory_icon'] or '',
            html=item_info['items.html'] or '',
            skip_query=true
        }
    )
   
    -- Save the original tag format:
    local item_tags_orig = {}
    for i,v in ipairs(tpl_args.item_tags) do
        item_tags_orig[i] = v
    end
   
    local item_mods = {}
    local mod_group_counter = {}
    mod_group_counter['all'] = {}
    local extra_fieldss = {}
    local table_index_base = -1 
    for _, sctn in ipairs(section) do
        item_mods[sctn['header']] = {}
       
        -- Preallocate the mod group counter, implicit and explicit mods
        -- are counted separetely because they can spawn together:
        mod_group_counter[sctn['header']] = {}
        local adj = 'explicit'
        if sctn['is_implicit'] then
            adj = 'implicit'
        end
        for _, header in ipairs({sctn['header'], 'all'}) do
            if mod_group_counter[header][adj] == nil then
                mod_group_counter[header][adj] = {}
            end
        end
       
        local continue = true
        local current_tags
        if sctn['tags'] then
            -- some item classes do not have shaper/elder items, so the table will not contain any tags:
            if #sctn['tags'] == 0 then
                continue = false
            else
                current_tags = sctn['tags']
            end
        else
            current_tags = {}
            -- Reset to original tags:
            for i,v in ipairs(item_tags_orig) do
                current_tags[i] = v
            end
        end
       
        if continue then           
            -- Cargo preparation:
            tpl_args.q_tables = 'mods, spawn_weights, mod_stats'
            tpl_args.q_fields = 'mods.name, mods.id, mods.required_level, mods.generation_type, mods.domain, mods.mod_group, mods.mod_type, mods.stat_text, mods.stat_text_raw, mods.tags, mods._pageName, mod_stats.id, spawn_weights.tag, spawn_weights.weight, spawn_weights.ordinal, spawn_weights._pageName'
            tpl_args.q_join = 'mods._pageName=spawn_weights._pageName, mods._pageName=mod_stats._pageName'
            tpl_args.q_where = sctn['where'](tpl_args, frame, value)
            tpl_args.q_groupBy = 'mods._pageName, spawn_weights.tag, spawn_weights.weight'
            tpl_args.q_orderBy = 'mods.generation_type, mods.mod_group, mods.mod_type, mods.required_level, mods._pageName, spawn_weights.ordinal'
           
            local extra_fields = {}
            if tpl_args.extra_fields ~= nil then
                extra_fields = m_util.string.split(tpl_args.extra_fields, ', ')
                tpl_args.q_fields = string.format(
                    '%s, %s',
                    tpl_args.q_fields,
                    table.concat(extra_fields, ', ')
                )
            end
           
            -- Query mods:
            results = h.cargo_query(tpl_args)
           
            -- Create own list for spawn weights and group by page name:
            local spawn_weights = {}
            local results_unique = {}
            local hash = {}
            for _,v in ipairs(results) do
                if spawn_weights[v['mods._pageName']] == nil then
                    spawn_weights[v['mods._pageName']] = {}
                end
                local n = #spawn_weights[v['mods._pageName']] or 0
                spawn_weights[v['mods._pageName']][n+1] = v
               
                -- Get a sorted list that only has unique page names:
                if hash[v['mods._pageName']] ~= true then
                    results_unique[#results_unique+1] = v
                    hash[v['mods._pageName']] = true
                end
            end
           
            if #results_unique > 0 then 
                -- Loop through all found modifiers:
                local last
                for _, v in ipairs(results_unique) do 
                    local pagename = v['spawn_weights._pageName']
                   
                    if sctn['header'] == i18n.drop_down_table.elder_prefix then
                    --  error(mw.dumpObject(spawn_weights))
                    end
                    -- Loop through all the modifier tags until they match
                    -- the item tags:
                    local j = 0
                    local tag_match_stop
                    repeat
                        j = j+1
                        local mod_tag = spawn_weights[pagename][j]['spawn_weights.tag']
                        local mod_tag_weight = tonumber(
                            spawn_weights[pagename][j]['spawn_weights.weight']
                        )
 
                        -- Loop through the item tags until it matches the
                        -- spawn weight tag and the mod tag has a value larger than
                        -- zero:
                        local y = 0
                        local tag_match_add = false
                        repeat   
                            y = y+1
                            tag_match_stop = ((mod_tag == current_tags[y]) and ((mod_tag_weight or -1) >= 0)) or (spawn_weights[pagename][j] == nil)
                            tag_match_add  =  (mod_tag == current_tags[y]) and ((mod_tag_weight or -1) > 0)
                        until tag_match_stop or y == #current_tags
                       
                        -- If there's a match then save that mod and other
                        -- interesting information:
                        if tag_match_add then
                           
                            -- Assume that the mod is global then go through
                            -- all the stat ids and check if any of the
                            -- stats are local:
                            local mod_scope = 'Global'
                            for _, vv in ipairs(spawn_weights[pagename]) do
                                if vv['mod_stats.id']:find('.*local.*') ~= nil then
                                    mod_scope = 'Local'
                                end
                            end
                           
                            -- Save the matching modifier tag:
                            local a = #item_mods[sctn['header']]
                            item_mods[sctn['header']][a+1] = spawn_weights[pagename][j]
                           
                            -- Save other interesting fields:
                            item_mods[sctn['header']][a+1]['mods.scope'] = mod_scope   
                            item_mods[sctn['header']][a+1]['spawn_weight.idx_match'] = j                       
                            item_mods[sctn['header']][a+1]['mods.add'] = tag_match_add   
                            item_mods[sctn['header']][a+1]['mods.stop'] = tag_match_stop
                           
                            -- Count the mod groups:
                            local group = item_mods[sctn['header']][a+1]['mods.mod_group'] or 'nil_group'
                            for _, header in ipairs({sctn['header'], 'all'}) do
                                if mod_group_counter[header][adj][group] == nil then
                                    mod_group_counter[header][adj][group] = {}
                                end
                                local tp = spawn_weights[pagename][j]['mods.mod_type']
                                local bef = mod_group_counter[header][adj][group][tp] or 0
                                mod_group_counter[header][adj][group][tp] = 1 + bef
                            end
                        end
                    until tag_match_stop
                end
               
                -- If the user wants to see the spawn chance then do the
                -- calculations and save that result as well:
                if tpl_args.spawn_chance ~= nil then
                    extra_fields[#extra_fields+1] = 'spawn_weights.chance'
                    item_mods[sctn['header']] = h.get_spawn_chance{
                        tbl = item_mods[sctn['header']],
                        chance_multiplier = sctn['chance_multiplier']
                    }
                end
               
                extra_fieldss[sctn['header']] = extra_fields
            end
        end
    end
   
   
    --
    -- Display the item mods
    --
   
    for _, sctn in ipairs(section) do
        local extra_fields = extra_fieldss[sctn['header']]
        local adj = 'explicit'
        if sctn['is_implicit'] then
            adj = 'implicit'
        end
       
        -- Create html container:
        local container = mw.html.create('div')
            :attr('style', 'vertical-align:top; display:inline-block;')
       
        -- Create the drop down table with <table></table>:
        local headers = container
        if #item_mods[sctn['header']] > 0 then
            headers
                :tag('h3')
                    :wikitext(string.format('%s', sctn['header']))
                    :done()
                :done()
        end
               
        local total_mod_groups = 0
        for _ in pairs(mod_group_counter[sctn['header']][adj]) do
            total_mod_groups = 1+total_mod_groups
        end
       
        -- Loop through and add all matching mods to the <table>.
        local tbl, last_group, last_type
        for _, rows in ipairs(item_mods[sctn['header']]) do 
       
            -- If the last mod group is different to the current
            -- mod group then assume the mod isn't related and start
            -- a new drop down list, if there's only one mod group
            -- then use mod type instead:
            if rows['mods.mod_group'] ~= last_group or (total_mod_groups == 1 and rows['mods.mod_type'] ~= last_type) then
                -- Check through all the mods and see if there are
                -- multiple mod types within the same mod group:
                local count = {}
                for _, n in ipairs(item_mods[sctn['header']]) do
                   
                    -- If the mod has the same mod group, then add
                    -- the mod type to the counter. Only unique mod
                    -- types matter so the number is just a dummy
                    -- value:
                    if n['mods.mod_group'] == rows['mods.mod_group'] then
                        count[n['mods.mod_type']] = 1
                    end
                end
               
                -- Calculate how many unique mod types with the
                -- same mod group there are for all explicit or implicit
                -- sections since a mod with the same mod group can't
                -- spawn. Doesn't matter if it's prefix or suffix.
                local number_of_mod_types = 0
                for _ in pairs(mod_group_counter['all'][adj][rows['mods.mod_group']]) do
                    number_of_mod_types = 1 + number_of_mod_types
                end
             
                -- If there are multiple unique mod types with the
                -- same mod group then change the style of the drop
                -- down list to indicate it, if there's only one
                -- mod group in the generation type then ignore it:
                local table_index_mod_group
                if number_of_mod_types > 1 and total_mod_groups > 1 then
                    table_index_mod_group = table.concat(
                        {string.byte(rows['mods.mod_group'], 1, #rows['mods.mod_group'])},
                        ''
                    )
                   
                    tbl_caption = string.format(
                        '%s',
                        m_util.html.poe_color(
                            'stat',
                            string.format(
                                '%s %s',
                                i18n.drop_down_table.mod_group,
                                rows['mods.mod_group']
                            )
                        ) or ''
                    )
 
                else
                    tbl_caption = string.format(
                        '%s (%s)',
                        m_util.html.poe_color(
                            'mod',
                            h.header(rows['mods.stat_text_raw'])
                        ) or '',
                        rows['mods.scope']
                    )
                end
                               
                -- Create a table index for handling the collapsible:
                table_index_base = table_index_base+1
                if table_index_mod_group ~= nil then
                    table_index = table_index_mod_group
                else
                    table_index = table_index_base
                end
               
                -- Add class and style to the <table>:
                tbl = container:tag('table')
                tbl
                    :attr('class', 'mw-collapsible mw-collapsed')
                    :attr('style',
                        'text-align:left; line-height:1.60em; width:810px;'
                    )
                    :tag('th')
                        :attr('class',
                            string.format('mw-customtoggle-%s', table_index)
                        )
                        :attr('style',
                            'text-align:left; line-height:1.40em; border-bottom:1pt solid dimgrey;'
                        )
                        :attr('colspan', '3' .. #extra_fields)
                        :wikitext(tbl_caption)
                        :done()
                    :done()   
            end
           
            -- If the mod has no name then use the mod type:
            local mod_name = rows['mods.name']
            if  mod_name == '' or mod_name == nil then
                mod_name = rows['mods.mod_type']
            end
           
            -- Check if there are any extra properties to show in
            -- the drop down list and then add a cell for that,
            -- add this node at the end of the table row:
            local td = mw.html.create('td')
            if extra_fields ~= nil then
                for _, extra_field in ipairs(extra_fields) do
                    td
                        :attr('width', '*')
                        :wikitext(string.format(
                            '%s:&nbsp;%s ',
                            extra_field,
                            rows[extra_field]
                            )
                        )
                        :done()
                end
            end
           
            -- Style mods.tags:
            local mods_tags = table.concat(m_util.string.split(rows['mods.tags'], ','), ', ')
            if mods_tags ~= '' then
                mods_tags = m_util.html.tooltip('*', mods_tags, class)
            end
           
            -- Add a table row with the interesting properties that
            -- modifier has:
            tbl
                :tag('tr')
                    :attr('class', 'mw-collapsible mw-collapsed')
                    :attr(
                        'id',
                        string.format('mw-customcollapsible-%s', table_index)
                    )
                    :tag('td')
                        :attr('width', '160')
                        :wikitext(
                            string.format(
                                '&nbsp;&nbsp;&nbsp;[[%s|%s]]',
                                rows['mods._pageName'],
                                mod_name:gsub('%s', '&nbsp;')
                            )
                        )
                        :done()
                    :tag('td')
                        :attr('width', '1')
                        :wikitext(
                            string.format(
                                '%s&nbsp;%s',
                                m_game.level_requirement['short_upper']
                                    :gsub('%s', '&nbsp;'),
                                rows['mods.required_level']
                            )
                        )
                        :done()       
                    :tag('td')
                        :attr('width', '*')
                        :wikitext(
                            string.format(
                                '%s%s',
                                m_util.html.poe_color(
                                    'mod',
                                    rows['mods.stat_text']
                                        :gsub('<br>', ', ')
                                        :gsub('<br />', ' ')
                                ) or '',
                                mods_tags
                            )
                        )
                        :done()
                    :node(td)
                    :done()
                :done()
           
            -- Save the last mod group for later comparison:
            last_group = rows['mods.mod_group']
            last_type = rows['mods.mod_type']
        end
        out[#out+1] = tostring(container)
    end
   
    return table.concat(out,'')
end
 
-- ----------------------------------------------------------------------------
-- Debug functions
-- ----------------------------------------------------------------------------
 
p.debug = {}
p.debug = {}
function p.debug.tbl_data(tbl)
function p.debug.tbl_data(tbl)
     keys = {}
     keys = {}
     for _, data in ipairs(mod_table.data) do
     for _, data in ipairs(mod_table.data) do
         if type(data.arg) == 'string' then  
         if type(data.arg) == 'string' then
             keys[data.arg] = 1
             keys[data.arg] = 1
         elseif type(data.arg) == 'table' then
         elseif type(data.arg) == 'table' then
Line 1,675: Line 630:
         end
         end
     end
     end
   
 
     for _, key in ipairs(mod_table.weights) do
     for _, key in ipairs(mod_table.weights) do
         keys[key] = 1
         keys[key] = 1
     end
     end
   
 
     local out = {}
     local out = {}
     for key, _ in pairs(keys) do
     for key, _ in pairs(keys) do
         out[#out+1] = string.format("['%s'] = '1'", key)
         out[#out+1] = string.format("['%s'] = '1'", key)
     end
     end
   
 
     return table.concat(out, ', ')
     return table.concat(out, ', ')
end
end
-- ----------------------------------------------------------------------------
--
-- ----------------------------------------------------------------------------


return p
return p

Latest revision as of 15:16, 21 October 2024

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


Implements {{Modifier table}}.

-------------------------------------------------------------------------------
-- 
--                           Module:Modifier table
-- 
-- Module responsible for displaying modifiers in various ways. Implements 
-- Template:Modifier table and Template:Item modifiers
-------------------------------------------------------------------------------

require('Module:No globals')
local m_util = require('Module:Util')
local m_cargo = require('Module:Cargo')

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

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

-- 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:Modifier table/config/sandbox') or mw.loadData('Module:Modifier table/config')

local i18n = cfg.i18n

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

local h = {}

function h.query_weights(table_name, page_ids)
    return m_cargo.map_results_to_id{
        results=m_cargo.query(
            {'mods', table_name},
            {
                'mods._pageID',
                table_name .. '.tag',
                table_name .. '.value'
            },
            {
                where=page_ids,
                join=string.format('mods._pageID=%s._pageID', table_name),
                orderBy=string.format('mods.id ASC,%s.ordinal ASC', table_name),
            }
        ),
        field='mods._pageID',
    }
end

h.tbl = {}
h.tbl.display = {}
h.tbl.display.factory = {}
function h.tbl.display.factory.value(args)
    -- Format options for each field:
    args.options = args.options or {}

    -- Separator between fields:
    args.delimiter = args.delimiter or ', '

    return function(tpl_args, tr, data, fields)
        local values = {}
        local fmt_values = {}

        for index, field in ipairs(fields) do
            local value = {
                min=data[field],
                max=data[field],
                base=data[field],
            }
            if value.min then
                values[#values+1] = value.max
                local opts = args.options[index] or {}
                -- Global colour is set, no overrides.
                if args.color ~= nil or opts.color == nil then
                    opts.no_color = true
                end
                fmt_values[#fmt_values+1] = m_util.html.format_value(nil, value, opts)
            end
        end

        if #values == 0 then
            tr
                :node(m_util.html.table_cell('na'))
        else
            local td = tr:tag('td')
            td
                :attr('data-sort-value', table.concat(values, args.delimiter))
                :wikitext(table.concat(fmt_values, args.delimiter))
            if args.color then
                td:attr('class', 'tc -' .. args.color)
            end
        end
    end
end

-- ----------------------------------------------------------------------------
-- Additional configuration
-- ----------------------------------------------------------------------------

local mod_table = {}
mod_table.data = {
    {
        arg = 'name',
        header = i18n.mod_table.name,
        fields = {'mods._pageName', 'mods.id', 'mods.name'},
        options = {
            [3] = {
                optional=true,
           },
        },
        display = function(tpl_args, tr, data, fields)
            local name
            if data['mods.name'] then
                name = data['mods.name']
            else
                name = data['mods.id']
            end

            tr
                :tag('td')
                    :wikitext(string.format('[[%s|%s]]', data['mods._pageName'], name))
        end,
        order = 1000,
        sort_type = 'text',
    },
    {
        arg = 'domain',
        header = i18n.mod_table.domain,
        fields = {'mods.domain'},
        display = function(tpl_args, tr, data, fields)
            local k = 'mods.domain'
            local i = tonumber(data[k])
            if m_game.constants.mod.domains[i] == nil then
                error('Undefined Modifier Domain ['..i..'] needs to be added to Module:Game')
            end
            data[k] = m_game.constants.mod.domains[i]['short_upper']
            h.tbl.display.factory.value{}(tpl_args, tr, data, fields)
        end,
        order = 2000,
        sort_type = 'text',
    },
    {
        arg = 'generation_type',
        header = i18n.mod_table.generation_type,
        fields = {'mods.generation_type'},
        display = function(tpl_args, tr, data, fields)
            local k = 'mods.generation_type'
            local i = tonumber(data[k])
            if m_game.constants.mod.generation_types[i] == nil then
                error('Undefined Modifier Generation Type ['..i..'] needs to be added to Module:Game')
            end
            data[k] = m_game.constants.mod.generation_types[i]['short_upper']
            h.tbl.display.factory.value{}(tpl_args, tr, data, fields)
        end,
        order = 2001,
        sort_type = 'text',
    },
    {
        arg = {'group', 'mod_group'},
        header = i18n.mod_table.mod_groups,
        fields = {'mods.mod_groups'},
        display = function(tpl_args, tr, data, fields)
            local k = 'mods.mod_groups'
            data[k] = table.concat(m_util.string.split(data[k], ',%s*'), ', ')
            h.tbl.display.factory.value{}(tpl_args, tr, data, fields)
        end,
        order = 2002,
        sort_type = 'text',
    },
    {
        arg = {'mod_type'},
        header = i18n.mod_table.mod_type,
        fields = {'mods.mod_type'},
        display = h.tbl.display.factory.value{},
        order = 2003,
        sort_type = 'text',
    },
    {
        arg = {'level', 'required_level'},
        header = i18n.mod_table.required_level,
        fields = {'mods.required_level'},
        display = h.tbl.display.factory.value{},
        order = 2004,
    },
    {
        arg = {'enchantment', 'labyrinth'},
        header = i18n.mod_table.labyrinth,
        fields = {string.format([[
                CONCAT(
                    CASE mods.required_level
                        WHEN 32 THEN "%s"
                        WHEN 53 THEN "%s"
                        WHEN 66 THEN "%s"
                        WHEN 75 THEN "%s"
                        WHEN 83 THEN "%s"
                    END
                )=labyrinth_text]],
            i18n.mod_table.normal_labyrinth,
            i18n.mod_table.cruel_labyrinth,
            i18n.mod_table.merciless_labyrinth,
            i18n.mod_table.eternal_labyrinth,
            i18n.mod_table.belt_labyrinth)
        },
        display = h.tbl.display.factory.value{},
        order = 2005,
        sort_type = 'text',
    },
    {
        arg = {'stat_text'},
        header = i18n.mod_table.stat_text,
        fields = {'mods.stat_text'},
        display = function(tpl_args, tr, data, fields)
            local value
            -- map display type shows this in another column, remove this text to avoid clogging up the list
            if tpl_args.type == 'map' then
                local texts = m_util.string.split(data['mods.stat_text'], '<br>')
                local out = {}

                local valid
                for _, v in ipairs(texts) do
                    valid = true
                    for _, data in pairs(mod_table.stat_ids) do
                        if string.find(v, data.pattern) ~= nil then
                            valid = false
                            break
                        end
                    end

                    if valid then
                        table.insert(out, v)
                    end
                end

                value = table.concat(out, '<br>')
            else
                value = data['mods.stat_text']
            end
            data['mods.stat_text'] = value
            h.tbl.display.factory.value{color='mod'}(tpl_args, tr, data, fields)
        end,
        order = 3000,
        sort_type = 'text',
    },
    {
        arg = 'buff',
        header = i18n.mod_table.buff,
        fields = {'mods.granted_buff_id', 'mods.granted_buff_value'},
        display = h.tbl.display.factory.value{delimiter=' '},
        order = 4000,
        sort_type = 'text',
    },
    {
        arg = {'skill', 'granted_skill'},
        header = i18n.mod_table.granted_skill,
        fields = {'mods.granted_skill'},
        display = h.tbl.display.factory.value{},
        order = 4001,
        sort_type = 'text',
    },
    {
        arg = {'tags'},
        header = i18n.mod_table.tags,
        fields = {'mods.tags'},
        display = function(tpl_args, tr, data, fields)
            local k = 'mods.tags'
            data[k] = table.concat(m_util.string.split(data[k], ',%s*'), ', ')
            h.tbl.display.factory.value{}(tpl_args, tr, data, fields)
        end,
        order = 5000,
        sort_type = 'text',
    },
}
mod_table.stat_ids = {
    ['map_item_drop_quantity_+%'] = {
        header = i18n.mod_table.iiq,
        pattern = '%d+%% increased Quantity of Items found in this Area',
    },
    ['map_item_drop_rarity_+%'] = {
        header = i18n.mod_table.iir,
        pattern = '%d+%% increased Rarity of Items found in this Area',
    },
    ['map_pack_size_+%'] = {
        header = i18n.mod_table.pack_size,
        pattern = '%+%d+%% Monster pack size',
    }
}
mod_table.weights = {'spawn_weights', 'generation_weights'}

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

local function _mod_table(tpl_args)
    --[[
    Creates a generic table for modifiers.

    Examples
    --------
    = p.mod_table{
        q_tables='mod_spawn_weights',
        q_join='mods._pageID=mod_spawn_weights._pageID',
        q_where='mods.generation_type = 10 AND mod_spawn_weights.tag = "boots" AND mod_spawn_weights.weight > 0',
        q_orderBy='mods.id, mods.required_level',
        q_limit=100,
        stat_text=1,
        enchantment=1,
    }
    ]]

    -- default to enabled
    tpl_args.name = tpl_args.name or true

    for _, key in ipairs(mod_table.weights) do
        tpl_args[key] = m_util.cast.boolean(tpl_args[key])
    end

    if string.find(tpl_args.q_where, '%[%[') ~= nil then
        error('SMW leftover in where clause')
    end

    local row_infos = {}
    for _, row_info in ipairs(mod_table.data) do
        local enabled = false
        if row_info.arg == nil then
            enabled = true
        elseif type(row_info.arg) == 'string' and m_util.cast.boolean(tpl_args[row_info.arg]) then
            enabled = true
        elseif type(row_info.arg) == 'table' then
            for _, argument in ipairs(row_info.arg) do
                if m_util.cast.boolean(tpl_args[argument]) then
                    enabled = true
                    break
                end
            end
        end

        if enabled then
            row_info.options = row_info.options or {}
            row_infos[#row_infos+1] = row_info
        end
    end

    -- sort the rows
    table.sort(row_infos, function (a, b)
        return (a.order or 0) < (b.order or 0)
    end)

    -- Set required and extra tables:
    local tables = {'mods'}
    for _, v in ipairs(m_util.string.split_args(tpl_args.q_tables, {',%s*'})) do
        tables[#tables+1] = v
    end

    -- Set required and extra fields:
    local fields = {
        'mods._pageID',
    }
    for _, row_info in ipairs(row_infos) do
        if type(row_info.fields) == 'function' then
            row_info.fields = row_info.fields()
        end
        for index, field in ipairs(row_info.fields) do
            row_info.options[index] = row_info.options[index] or {}
            fields[#fields+1] = field
        end
    end
    tpl_args._extra_fields = m_util.string.split_args(tpl_args.q_fields, {',%s*'})
    for _, v in ipairs(tpl_args._extra_fields) do
        fields[#fields+1] = v
    end

    -- Parse query arguments:
    local query = {
        -- Workaround: fix duplicates
        groupBy='mods._pageID',
    }
    for key, value in pairs(tpl_args) do
        if string.sub(key, 0, 2) == 'q_' then
            query[string.sub(key, 3)] = value
        end
    end

    local results = m_cargo.query(tables, fields, query)

    if #results == 0 then
        if tpl_args.default ~= nil then
            return tpl_args.default
        else
            return 'No results found'
        end
    end

    -- this might be needed in other queries, currently not checking if
    -- it's actually needed because performance impact should be neglible
    local page_ids = {}
    for _, row in ipairs(results) do
        page_ids[#page_ids+1] = string.format(
            'mods._pageID="%s"',
            row['mods._pageID']
        )
    end
    page_ids = table.concat(page_ids, ' OR ')

    local weights = {}
    for _, key in ipairs(mod_table.weights) do
        if tpl_args[key] then
            weights[key] = h.query_weights('mod_' .. key, page_ids)
        end
    end

    local stats
    if tpl_args.type == 'map' then
        local query_stat_ids = {}
        for k, _ in pairs(mod_table.stat_ids) do
            query_stat_ids[#query_stat_ids+1] = string.format('mod_stats.id="%s"', k)
        end

        stats = m_cargo.map_results_to_id{
            results=m_cargo.query(
                {'mods', 'mod_stats'},
                {
                    'mods._pageID',
                    'mod_stats.id',
                    'mod_stats.min',
                    'mod_stats.max',
                },
                {
                    where=string.format(
                        '(%s) AND (%s)',
                        page_ids,
                        table.concat(query_stat_ids, ' OR ')
                    ),
                    join='mods._pageID=mod_stats._pageID',
                    orderBy='mods.id ASC',
                }
            ),
            field='mods._pageID'
        }

        -- In addition map stats to stat <-> min/max pairs
        for page_id, rows in pairs(stats) do
            local stat_id_map = {}
            for _, row in ipairs(rows) do
                stat_id_map[row['mod_stats.id']] = {
                    min=tonumber(row['mod_stats.min']),
                    max=tonumber(row['mod_stats.max'])
                }
            end
            stats[page_id] = stat_id_map
        end
    end

    --
    -- Display
    --

    -- Preformance optimization
    for index, field in ipairs(tpl_args._extra_fields) do
        field = m_util.string.split(field, '%s*=%s*')
        -- field[2] will be nil if there is no alias
        tpl_args._extra_fields[index] = field[2] or field[1]
    end

    local tbl = mw.html.create('table')
    tbl:attr('class', 'wikitable sortable modifier-table')
    -- Header

    local tr = tbl:tag('tr')
    local display_fields = {}
    for i, row_info in ipairs(row_infos) do
        for j, field in ipairs(row_info.fields) do
            -- Aliased name is used as keys in the results:
            field = m_util.string.split(field, '%s*=%s*')
            field = field[2] or field[1]

            -- Make a new field table since mod_table.data will remain
            -- modified the 2nd time a function is run and then crash.
            if j == 1 then
                display_fields[i] = {}
            end
            display_fields[i][j] = field
        end

        tr
            :tag('th')
                :attr('data-sort-type', row_info.sort_type or 'number')
                :wikitext(row_info.header)
                :done()
    end

    if tpl_args.type == 'map' then
        for stat_id, data in pairs(mod_table.stat_ids) do
            tr
                :tag('th')
                    :attr('data-sort-type', 'number')
                    :wikitext(data.header)
                    :done()
        end
    end

    for _, key in ipairs(mod_table.weights) do
        if tpl_args[key] then
            tr
                :tag('th')
                    :wikitext(i18n.mod_table[key])
        end
    end

    for _, field in ipairs(tpl_args._extra_fields) do
        tr
            :tag('th')
                :wikitext(field)
    end

    -- Body

    for _, row in ipairs(results) do
        tr = tbl:tag('tr')

        for i, row_info in ipairs(row_infos) do
            -- this has been cast from a function in an earlier step
            local display = true
            for j, field in ipairs(display_fields[i]) do
                -- this will bet set to an empty value not nil confusingly
                if row[field] == nil or row[field] == '' then
                    if row_info.options[j].optional ~= true then
                        display = false
                        break
                    else
                        row[field] = nil
                    end
                end
            end
            if display then
                row_info.display(tpl_args, tr, row, display_fields[i])
            else
                tr:node(m_util.html.table_cell('na'))
            end
        end

        if tpl_args.type == 'map' then
            for stat_id, data in pairs(mod_table.stat_ids) do
                local stat_data = stats[row['mods._pageID']]
                if stat_data and stat_data[stat_id] then
                    local v = stat_data[stat_id]
                    local text
                    if v.min == v.max then
                        text = v.min
                    else
                        text = string.format('(%s to %s)', v.min, v.max)
                    end
                    tr
                        :tag('td')
                            :attr('data-sort-value', (v.min+v.max)/2)
                            :wikitext(string.format('%s%%', text))
                            :done()
                else
                    tr:node(m_util.html.table_cell('na'))
                end
            end
        end

        for _, key in ipairs(mod_table.weights) do
            if tpl_args[key] then
                local weight_out = {}
                local fields = {
                    tag = string.format('mod_%s.tag', key),
                    value = string.format('mod_%s.value', key),
                }
                for _, wrow in ipairs(weights[key][row['mods._pageID']]) do
                    if wrow[fields.tag] and wrow[fields.value] then
                        weight_out[#weight_out+1] = string.format(
                            '%s %s',
                            wrow[fields.tag],
                            wrow[fields.value]
                        )
                    end
                end

                if #weight_out > 0 then
                    tr
                        :tag('td')
                            :wikitext(table.concat(weight_out, '<br>'))
                            :done()
                else
                    tr:node(m_util.html.table_cell('na'))
                end
            end
        end

        for _, field in ipairs(tpl_args._extra_fields) do
            if row[field] then
                tr
                    :tag('td')
                        :wikitext(row[field])
            else
                tr:node(m_util.html.table_cell('na'))
            end
        end
    end

    return tostring(tbl)
end

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

local p = {}

-- 
-- Template:Modifier table
-- 
p.mod_table = m_util.misc.invoker_factory(_mod_table, {
    parentFirst = true,
})

-- 
-- Debug
-- 
p.debug = {}
function p.debug.tbl_data(tbl)
    keys = {}
    for _, data in ipairs(mod_table.data) do
        if type(data.arg) == 'string' then
            keys[data.arg] = 1
        elseif type(data.arg) == 'table' then
            for _, arg in ipairs(data.arg) do
                keys[arg] = 1
            end
        end
    end

    for _, key in ipairs(mod_table.weights) do
        keys[key] = 1
    end

    local out = {}
    for key, _ in pairs(keys) do
        out[#out+1] = string.format("['%s'] = '1'", key)
    end

    return table.concat(out, ', ')
end

return p