Module:Modifier table: Difference between revisions

From Path of Exile Wiki
Jump to navigation Jump to search
>Illviljan
(Simplified copy/pasteing modids.)
No edit summary
 
(79 intermediate revisions by 7 users not shown)
Line 1: Line 1:
-- Module responsible for the old fashioned mod tables
-------------------------------------------------------------------------------
--
--                          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_cargo = require('Module:Cargo')
local m_game = require('Module:Game')


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 = {
local h = {}
    mod_table = {
 
        name = m_util.html.abbr('Name', 'Name of the modifier if available or its internal identifier instead'),
function h.query_weights(table_name, page_ids)
         mod_group = m_util.html.abbr('Group', 'Only one modifier from the specified group can appear at a time under normal circumstances'),
    return m_cargo.map_results_to_id{
        mod_type = 'Type',
         results=m_cargo.query(
        domain = '[[Modifiers#Mod_Domain|Domain]]',
            {'mods', table_name},
        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.]]',
                'mods._pageID',
        stat_text = m_util.html.abbr('Stats', 'Stats of the modifier and the range they can roll in (if applicable)'),
                table_name .. '.tag',
        buff = m_util.html.abbr('Buff', 'ID of the buff granted and the values associated'),
                table_name .. '.value'
        granted_skill = m_util.html.abbr('Skill', 'ID of the skill granted'),
            },
        tags = '[[Tags]]',
            {
       
                where=page_ids,
        iiq = m_util.html.abbr('IIQ', 'increased Quantity of Items found in this Area'),
                join=string.format('mods._pageID=%s._pageID', table_name),
        iir = m_util.html.abbr('IIR', 'increased Rarity of Items found in this Area'),
                orderBy=string.format('mods.id ASC,%s.ordinal ASC', table_name),
        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'),
         field='mods._pageID',
         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'),
     }
     },
end
   
 
    errors = {
h.tbl = {}
        --
h.tbl.display = {}
        -- Mod template
h.tbl.display.factory = {}
        --
function h.tbl.display.factory.value(args)
        sell_price_duplicate_name = 'Do not specify a sell price item name multiple times. Adjust the amount instead.',
    -- Format options for each field:
        sell_price_missing_argument = 'Both %s and %s must be specified',
    args.options = args.options or {}
       
        --
        -- 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.',
    },
}


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


--
    return function(tpl_args, tr, data, fields)
-- Helper/Utility functions
        local values = {}
--
        local fmt_values = {}


local h = {}
        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


function h.query_weights(table_name, page_ids)
        if #values == 0 then
    results = cargo.query(
            tr
        string.format('mods,%s', table_name),
                :wikitext(m_util.html.td.na())
        string.format('mods._pageID,%s.tag,%s.weight', table_name, table_name),
         else
         {
             local td = tr:tag('td')
             where=page_ids,
             td
            join=string.format('mods._pageID=%s._pageID', table_name),
                :attr('data-sort-value', table.concat(values, args.delimiter))
             orderBy=string.format('mods.id ASC,%s.ordinal ASC', table_name),
                :wikitext(table.concat(fmt_values, args.delimiter))
            limit=5000,
            if args.color then
        }
                td:attr('class', 'tc -' .. args.color)
    )
            end
    if #results == 5000 then
        end
        error('Hit maximum cargo results')
     end
     end
   
    return m_util.cargo.map_results_to_id{results=results, table_name='mods'}
end
function h.disambiguate_mod_name(results)
    --[[
        Disambiguates results from a mods query.
    ]]
    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'],
            string.gsub(
                v['mods.stat_text_raw'],
                '<br>',
                ', '
            ) or '',
            v['mods._pageName']
        )
    end
    return table.concat(str, '<br>')
end
end


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


Line 107: Line 100:
mod_table.data = {
mod_table.data = {
     {
     {
         arg = nil,
         arg = 'name',
         header = i18n.mod_table.name,
         header = i18n.mod_table.name,
         fields = {'mods._pageName', 'mods.id', 'mods.name'},
         fields = {'mods._pageName', 'mods.id', 'mods.name'},
Line 115: 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 122: Line 115:
                 name = data['mods.id']
                 name = data['mods.id']
             end
             end
             tr
             tr
                 :tag('td')
                 :tag('td')
Line 133: 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 147: 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 159: 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 173: 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 185: 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,
    },
    {
        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',
     },
     },
     {
     {
Line 196: 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 212: 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 247: 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 259: 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 284: Line 286:
mod_table.weights = {'spawn_weights', 'generation_weights'}
mod_table.weights = {'spawn_weights', 'generation_weights'}


function p.mod_table(frame)
-- ----------------------------------------------------------------------------
    tpl_args = getArgs(frame, {
-- Main functions
         parentFirst = true
-- ----------------------------------------------------------------------------
     })
 
     frame = m_util.misc.get_frame(frame)
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
     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 305: 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 313: 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 319: 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 = cargo.query(
     local results = m_cargo.query(tables, fields, query)
        tables,
 
        table.concat(fields, ','),
        query
    )
   
     if #results == 0 then
     if #results == 0 then
         if tpl_args.default ~= nil then
         if tpl_args.default ~= nil then
Line 370: 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 392: 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, table_name='mods'}
                    ),
                    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
    --
 
    -- 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')
     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 431: 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 441: 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 449: Line 505:
         end
         end
     end
     end
      
 
     for _, field in ipairs(tpl_args._extra_fields) do
        tr
            :tag('th')
                :wikitext(field)
    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 470: 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:wikitext(m_util.html.td.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 497: Line 559:
             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:wikitext(m_util.html.td.na())
                 end
                 end
             end
             end
         end
         end
    end
   
    return tostring(tbl)
end


function p.modifier_link(frame)
         for _, field in ipairs(tpl_args._extra_fields) do
    --[[
             if row[field] then
    Finds and links to a modifier in formatted form.
                 tr
   
                    :tag('td')
    Examples:
                        :wikitext(row[field])
    = p.modifier_link{"Tyrannical"}
             else
    = p.modifier_link{"Flaring"}
                tr:wikitext(m_util.html.td.na())
    = p.modifier_link{"Dictator's"}
    = 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)
       
    -- Synonyms:
    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._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="%s" or mods.id="%s") AND mod_stats.id LIKE "%%%s%%"',
            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 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
    end
   
    -- Helpful error handling:
    local err_tbl = {
        {
            b = #results == 0,
            d = {
                i18n.errors.no_results,
            }
        },
        {
            b = #tpl_args.results_unique > 1,
            d = {
                i18n.errors.multiple_results,
                h.disambiguate_mod_name(tpl_args.results_unique),
            },
        },
        {
            b = tpl_args.modid ~= tpl_args.results_unique[1]['mods.id'],
            d = {
                string.gsub(
                    i18n.errors.incorrect_modid,
                    '%%s',
                    tpl_args.modid,
                    1
                ),
                h.disambiguate_mod_name(tpl_args.results_unique),
            },
        },
    }
    for i,v in ipairs(err_tbl) do
        if v.b then
            return m_util.html.error({msg = string.format(v.d[1], v.d[2])})
         end
         end
     end
     end
   
 
   
     return tostring(tbl)
     -- Display formats:
    local display = {
        abbr = {
            display = function(tpl_args, frame)
                return string.format(
                    '%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.abbr(
                        '[?]',
                        string.gsub(
                            tpl_args.results_unique[1]['mods.stat_text_raw'],
                            '<br>',
                            ', '
                        ),
                        class
                    )
                )
            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
end


-- ----------------------------------------------------------------------------
-- ----------------------------------------------------------------------------
-- Debug functions
-- Exported functions
-- ----------------------------------------------------------------------------
-- ----------------------------------------------------------------------------


local p = {}
--
-- Template:Modifier table
--
p.mod_table = m_util.misc.invoker_factory(_mod_table, {
    parentFirst = true,
})
--
-- Debug
--
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 705: 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 16:47, 6 December 2022

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
                :wikitext(m_util.html.td.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:wikitext(m_util.html.td.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:wikitext(m_util.html.td.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:wikitext(m_util.html.td.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:wikitext(m_util.html.td.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