Module:Item2: Difference between revisions

From Path of Exile Wiki
Jump to navigation Jump to search
>OmegaK2
(essence tier support)
>OmegaK2
(rename tier to level)
Line 1,127: Line 1,127:
         func = core.factory.number_cast('essence_level_restriction'),
         func = core.factory.number_cast('essence_level_restriction'),
     },
     },
     essence_tier = {
     essence_level = {
         property = 'Has essence tier',
         property = 'Has essence level',
         func = core.factory.number_cast('essence_tier'),
         func = core.factory.number_cast('essence_level'),
     },
     },
     --
     --
Line 1,640: Line 1,640:
     },
     },
     ['Stackable Currency'] = {
     ['Stackable Currency'] = {
         args = {'is_essence', 'essence_level_restriction', 'essence_tier'},
         args = {'is_essence', 'essence_level_restriction', 'essence_level'},
         frame_type = 'currency',
         frame_type = 'currency',
     },
     },
Line 2,306: Line 2,306:
         -- Essence stuff
         -- Essence stuff
         {
         {
             args = {'essence_tier'},
             args = {'essence_level'},
             func = core.factory.display_value{
             func = core.factory.display_value{
                 keys={'essence_tier'},
                 keys={'essence_level'},
                 options = {
                 options = {
                     [1] = {
                     [1] = {
                         fmt = '%i',
                         fmt = '%i',
                         before = 'Essence Tier: ',
                         before = 'Essence Level: ',
                     },
                     },
                 },
                 },

Revision as of 08:24, 30 August 2016

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


This module is used on 13,000+ pages.

To avoid major disruption and server load, do not make unnecessary edits to this module. Test changes to this module first using its /sandbox and /testcases subpages . All of the changes can then be applied to this module in a single edit.

Consider discussing changes on the talk page or on Discord before implementing them.

The module implements {{item}}.

Overview

This module is responsible for creating item boxes, various item lists, item links and other item-related tasks. In the process a lot of the input data is verified and also added as semantic property to pages; as such, any templates deriving from this module should not be used on user pages other then for temporary testing purposes.

This template is also backed by an export script in PyPoE which can be used to export item data from the game files which then can be used on the wiki. Use the export when possible. de:Modul:Item2 ru:Модуль:Item2

-- SMW reworked item module

-- TODO: 
-- manual stats for uniques
-- drop location (+drop difficulty) support
-- divinatation card support
-- talisman tier and category
-- currency: description, stack_size_currency_tab, cosmetic_type
-- potentially include quality bonus in calcuations
-- singular for weapon class in infoboxes
-- Maps: Area level can be retrived eventually


-- ----------------------------------------------------------------------------
-- Imports
-- ----------------------------------------------------------------------------
local xtable = require('Module:Table')
local util = require('Module:Util')
local getArgs = require('Module:Arguments').getArgs
local m_game = require('Module:Game')
local m_skill = require('Module:Skill')

local p = {}
local v = {}
local g_frame, g_args


local image_size = 39

-- ----------------------------------------------------------------------------
-- Other stuff
-- ----------------------------------------------------------------------------

local h = {}

function h.debug(func)
    if g_args.debug ==  nil then
        return
    end
    func()
end

function h.new_color(label, text)
    if text == nil or text == '' then
        return nil
    end
    return tostring(mw.html.create('span')
        :attr('class', 'text-color -' .. label)
        :wikitext(text))
end

function h.na_or_val(tr, value, func)
    if value == nil then
        tr:wikitext(util.html.td.na())
    else
        local raw_value = value
        if func ~= nil then
            value = func(value)
        end
        tr
            :tag('td')
                :attr('data-sort-value', raw_value)
                :wikitext(value)
                :done()
    end
end

-- helper to loop over the range variables easier
h.range_map = {
    min = {
        property = ' range minimum',
        var = '_range_minimum',
    },
    max = {
        property = ' range maximum',
        var = '_range_maximum',
    },
    avg = {
        property = ' range average',
        var = '_range_average',
    },
}

function h.stats_update(id, value, modid)
    if g_args._stats[id] == nil then
        value.references = {modid}
        g_args._stats[id] = value
    else
        if modid ~= nil then
            table.insert(g_args._stats[id].references, mod_data.result['Is Mod'])
        end
        g_args._stats[id].min = g_args._stats[id].min + value.min
        g_args._stats[id].max = g_args._stats[id].max + value.max
        g_args._stats[id].avg = g_args._stats[id].avg + value.avg
    end
end

h.stat = {}
function h.stat.add (value, stat_cached) 
    value.min = value.min + stat_cached.min
    value.max = value.max + stat_cached.max
end

function h.stat.more (value, stat_cached)
    value.min = value.min * (1 + stat_cached.min / 100)
    value.max = value.max * (1 + stat_cached.max / 100)
end

h.tbl = {}
h.tbl.display = {}
function h.tbl.display.na_or_val(tr, value, data)
    return h.na_or_val(tr, value)
end

function h.tbl.display.seconds(tr, value, data)
    return h.na_or_val(tr, value, function(value)
        return string.format('%ss', value)
    end)
end

function h.tbl.display.percent(tr, value, data)
    return h.na_or_val(tr, value, function(value)
        return string.format('%s%%', value)
    end)
end

function h.tbl.display.wikilink(tr, value, data)
    return h.na_or_val(tr, value, function(value)
        return string.format('[[%s]]', value)
    end)
end

h.tbl.display.factory = {}
function h.tbl.display.factory.range(args)
   -- args: table
   --  property
   --  no_base: no base value given
   --  options: table, see h.format_value
   args.options = args.options or {}
   
   return function (tr, value, data)
        local value = {}
        local chk_keys = {}
        
        for key, range_data in pairs(h.range_map) do
            value[key] = tonumber(data[string.format('?Has %s%s', args.property, range_data.property)])
            chk_keys[#chk_keys+1] = key
        end
        
        if args.no_base == nil then
            value.base = tonumber(data['?Has base ' .. args.property])
            chk_keys[#chk_keys+1] = 'base'
        end
        
        if util.table.has_one_value(value, chk_keys, nil) then
            tr:wikitext(util.html.td.na())
            return
        end
        
        if args.no_base or value.avg == value.base then
            args.options.color = 'value'
        else
            args.options.color = 'mod'
        end
        
        tr
            :tag('td')
                :attr('data-sort-value', value.avg)
                :wikitext(h.format_value(value, args.options))
                :done()
   end
end

function h.format_value(value, options)
    -- value: table
    --  min:
    --  max:
    -- options: table
    --  fmt: formatter to use for the value instead of valfmt
    --  before: add this string before the coloured string with colour gray
    --  after: add this string after the coloured string with colour gray
    --  func: Function to adjust the value with before output
    --  color: colour code for h.new_color, overrides mod colour
    if options.color then
        value.color = options.color
    elseif value.base ~= value.min or value.base ~= value.max then
        value.color = 'mod'
    else
        value.color = 'value'
    end
    
    if options.func ~= nil then
        value.min = options.func(value.min)
        value.max = options.func(value.max)
    end
    
    if options.fmt == nil then
        options.fmt = '%s'
    elseif type(options.fmt) == 'function' then
        options.fmt = options.fmt()
    end
    
    if value.min == value.max then
        value.out = string.format(options.fmt, value.min)
    else
        value.out = string.format(string.format("(%s to %s)", options.fmt, options.fmt), value.min, value.max)
    end
    
    value.out = h.new_color(value.color, value.out)
    
    local before = ''
    if type(options.before) == 'string' then
        before = h.new_color('default', options.before)
    elseif type(options.before) == 'function' then
        before = h.new_color('default', options.before())
    end
    
    local after = ''
    if type(options.after) == 'string' then
        after = h.new_color('default', options.after)
    elseif type(options.after) == 'function' then
        after = h.new_color('default', options.after())
    end
    
    return before .. value.out .. after
end

-- ----------------------------------------------------------------------------
-- core
-- ----------------------------------------------------------------------------

local core = {}

function core.validate_mod(args)
    -- args:
    --  key   - implict or explicit
    --  i
    --  value
    local value = g_args[args.key .. args.i]
    local out = {
        result=nil,
        modid=nil,
        type=args.key,
    }
    
    if value ~= nil then
        table.insert(g_args.mods, value)
        table.insert(g_args[args.key .. '_mods'], value)
        out.modid = value
        out.result = {}
    else
        value = g_args[args.key .. args.i .. '_text']
        if value ~= nil then
            --table.insert(g_args._subobjects, {
            --    ['Is mod number'] = args.i,
            --   ['Has mod text'] = value,
            --})
            out.result = value
        end
    end
    
    if out.result ~= nil then
        table.insert(g_args._mods, out)
        return true
    else
        return false
    end
end

function core.process_smw_mods()
    if #g_args.mods == 0 then 
        return
    end
    
    local mods = {}
    for _, modid in ipairs(g_args.mods) do
        for _, mod_data in ipairs(g_args._mods) do
            if mod_data.modid == modid then
                mods[modid] = mod_data
                break
            end
        end
    end
    
    local query = {
        string.format('[[Is mod::%s]]', table.concat(g_args.mods, '||')),
        '?#=Page',
        '?Is mod#',
        '?Has mod group#',
        '?Has mod type#',
        '?Has stat text#',
        '?Has level requirement#',
    }
    
    local results = util.smw.query(query, g_frame)
    local pages = {}
    
    -- remap this as table with modids as key
    for _, result in ipairs(results) do
        local mod_data = mods[result['Is mod']]
        mod_data.result = result
        
        -- needed for the query
        pages[#pages+1] = result[1]
        -- needed for the mapping stats to mods
        pages[result[1]] = mod_data
        
        -- update item level requirement
        local keys = {'required_level_final'}
        -- only update base item requirement if this is an implicit
        if mod_data.key == 'implicit' then
            keys[#keys+1] = 'required_level'
        end
        
        for _, key in ipairs(keys) do
            local req = math.floor(tonumber(result['Has level requirement']) * 0.8)
            if req > g_args[key] then
                g_args[key] = req
            end
        end
    end
    
    -- TODO: Can items have mods twice? I dont think so, if they do it would need to be accounted for here
    if results == nil or #results ~= #g_args.mods then
        local missing = {}
        for _, modid in ipairs(g_args.mods) do
            if mods[modid].result == nil then
                missing[#missing+1] = modid
            end
        end
        error(string.format('Mod(s) with ids "%s" were not found', table.concat(missing, ', ')))
    end
    
    -- fetch stats
    
    query = {
        string.format('[[-Has subobject::%s]] [[Is stat number::+]]', table.concat(pages, '||')),
        '?Is stat number#',
        '?Has stat id#',
        '?Has minimum stat value#',
        '?Has maximum stat value#',
    }
    
    local stats = util.smw.query(query, g_frame)
    
    -- process and cache stats
    for _, stat in ipairs(stats) do
        local mod_data = pages[util.string.split(stat[1], '#')[1]]
        if mod_data.result.stats == nil then
            mod_data.result.stats = {stat, }
        else
            mod_data.result.stats[#mod_data.result.stats+1] = stat
        end
    
        local id = stat['Has stat id']
        local value = {
            min = tonumber(stat['Has minimum stat value']),
            max = tonumber(stat['Has maximum stat value']),
        }
        value.avg = (value.min+value.max)/2
        
        h.stats_update(id, value, mod_data.result['Is Mod'])
    end
end

function core.process_base_item(args)
    local query = {}

    if g_args.base_item_id ~= nil then
        query[#query+1] = string.format('[[Has metadata id::%s]]', g_args.base_item_id)
    elseif g_args.base_item_page ~= nil then
        query[#query+1] = string.format('[[%s]]', g_args.base_item_page)
    elseif g_args.base_item ~= nil then
        query[#query+1] = string.format('[[Has name::%s]]', g_args.base_item)
    elseif g_args.rarity ~= 'Normal' then
        error(core.err{msg='Rarity is set to above normal, but base item is not set. A base item for rarities above normal is required!'})
    else
        return
    end
    
    if #query > 1 and g_args.rarity == 'Normal' then
        error(core.err{msg='Base item parameter is set, but rarity is set to normal. A rarity above normal is required!'})
    end
    
    query[#query+1] = string.format('[[Has item class::%s]]', g_args['class'])
    query[#query+1] = '[[Has rarity::Normal]]'
    query[#query+1] = '?Has implicit mod ids#'
    query[#query+1] = '?Has metadata id'
    query[#query+1] = '?Has name'
    
    for _, k in ipairs(g_args._base_item_args) do
        query[#query+1] = string.format('?%s#', core.map[k].property)
    end
    
    local result = util.smw.query(query, g_frame)
    
    if #result > 1 then
        error(core.err{msg='More then one result found for the specified base item. Consider using base_item_page or base_item_id to narrow down the results.'})
        -- TODO be more explicit in the error?
    end
    
    result = result[1]

    --Copy values..
    for _, k in ipairs(g_args._base_item_args) do
        local data = core.map[k]
        local value = result[data.property]
        -- I can just use data.default since it will be nil if not provided. Neat! ;)
        if value ~= "" and g_args[k] == data.default then
            g_args[k] = value
            if data.property_func ~= nil then
                data.property_func()
            elseif data.func ~= nil then
                data.func()
            end
        elseif value == "" then
            h.debug(function ()
                mw.logObject(string.format("Base item property not found: %s", data.property))
            end)
        elseif g_args[k] ~= data.default then
            h.debug(function ()
                mw.logObject(string.format("Value for arg '%s' is set to something else then default: %s", k, tostring(g_args[k])))
            end)
        end
    end
    
    g_args.base_item_data = result
    core.process_arguments{array={'base_item', 'base_item_page', 'base_item_id'}} 
end

function core.process_arguments(args)
    for _, k in ipairs(args.array) do
        local data = core.map[k]
        table.insert(g_args._total_args, k)
        if data.no_copy == nil then
            table.insert(g_args._base_item_args, k)
        end
        if data.func ~= nil then
            local status, err = pcall(data.func)
            -- an error was raised, return the error string instead of the template
            if not status then
                return err
            end
        end
        if data.default ~= nil and g_args[k] == nil then
            g_args[k] = data.default
        end
    end
end
        
function core.process_mod_stats(args)
    local lines = {}
    
    local skip = core.class_specifics[g_args.class]
    if skip then
        skip = skip.skip_stat_lines
    end 
    
    for _, modinfo in ipairs(g_args._mods) do
        if modinfo.type == args.type then
            if modinfo.modid == nil then
                table.insert(lines, modinfo.result)
            else
                for _, line in ipairs(util.string.split(modinfo.result['Has stat text'], '<br>')) do
                    if skip == nil then
                        table.insert(lines, line)
                    else
                        local skipped = false
                        for _, pattern in ipairs(skip) do
                            if string.match(line, pattern) then
                                skipped = true
                                break
                            end
                        end
                        if not skipped then
                            table.insert(lines, line)
                        end
                    end
                end
            end
        end
    end
    
    if #lines == 0 then
        return
    else
        return table.concat(lines, '<br>')
    end
end

function core.err(args)
    local err = mw.html.create('div')
    err
        :attr('style', 'font-color: red; font-weight: bold;')
        :wikitext(args.msg or ('Argument ' .. args.key .. ' to item template is invalid. Please check the documention for acceptable values.'))
        :done()
        
    return tostring(err)
end

--
-- function factory
--
core.factory = {}
function core.factory.array_table_cast(k, args)
    -- Arguments:
    --
    -- tbl
    -- errmsg
    return function ()
        local elements
        
        if g_args[k] ~= nil then
            elements = util.string.split(g_args[k], ', ')
            for _, element in ipairs(elements) do 
                local r = util.table.find_in_nested_array{value=element, tbl=args.tbl, key='full'}
                if r == nil then
                    error(core.err{msg=string.format(args.errmsg, element)})
                end
            end
            g_args[k] = xtable:new(elements)
        end
    end
end

function core.factory.table_cast(k, args)
    return function ()
        args.value = g_args[k]
        g_args[k] = util.table.find_in_nested_array(args)
        if g_args[k] == nil then
            error(core.err{key=k})
        end
    end
end


function core.factory.number_cast(k)
    return function ()
        g_args[k] = tonumber(g_args[k])
    end
end

function core.factory.boolean_cast(k)
    return function()
        if g_args[k] ~= nil then
            g_args[k] = util.cast.boolean(g_args[k])
        end
    end
end

function core.factory.percentage(k)
    return function ()
        local v = tonumber(g_args[k])
        
        if v == nil then
            return core.err{key=k}
        end
        
        if v < 0 or v > 100 then
            return core.err{msg=k .. ' must be in range 0-100.'}
        end
        
        g_args[k] = v
    end
end

function core.factory.display_value(args)
    -- TODO: sane defaults for the prepend string "before" & after arguments
    --
    -- args:
    --  type: Type of the keys (nil = regular, gem = skill gems, stat = stats)
    --  args<Array>: Array of arguments to use for displaying
    --  values<Array>: Array of base values to use for displaying; useful if no args are present
    --  options<Array>:
    --   fmt: formatter to use for the value instead of valfmt
    --   allow_zero: allow zero values
    --   before: add this string before the coloured string with colour gray
    --   after: add this string after the coloured string with colour gray
    --   func: Function to adjust the value with before output
    --   hide_default: hide the value if this is set;
    --   color: colour code for h.new_color, overrides mod colour

    
    for k, default in pairs({stats = {}, options = {}}) do
        if args[k] == nil then
            args[k] = default
        end
    end
    
    local size
    if args.keys ~= nil then
        size = #args.keys
    elseif args.values ~= nil then
        size = #args.values
    end
    
    if size then
        for i=1, size do
            if args.options[i] == nil then
                args.options[i] = {}
            end
        end
    else
        error('invalid data')
    end
    
    return function ()
        local base_values = {}
        local temp_values = {}
        if args.type == 'gem' then
            if not core.class_groups.gems.keys[g_args.class] then
                return
            end
            for i, k in ipairs(args.keys) do
                local value = g_args['static_' .. k]
                if value ~= nil then
                    base_values[#base_values+1] = value
                    temp_values[#temp_values+1] = {value={min=value,max=value}, index=i}
                else
                    value = {
                        min=g_args[string.format('level1_%s', k)],
                        max=g_args[string.format('level%s_%s', g_args.max_level, k)], 
                    }
                    if value.min == nil or value.max == nil then
                    else
                        base_values[#base_values+1] = value.min
                        temp_values[#temp_values+1] = {value=value, index=i}
                    end
                end
            end
        elseif args.type == 'stat' then
            for i, k in ipairs(args.keys) do
                local value = g_args._stats[k]
                if value ~= nil then
                    base_values[i] = value.min
                    temp_values[#temp_values+1] = {value=value, index=i}
                end
            end
        else
            for i, k in ipairs(args.keys) do
                base_values[i] = g_args[k]
                local value = {}
                if g_args[k .. '_range_minimum'] ~= nil then
                    value.min = g_args[k .. '_range_minimum']
                    value.max = g_args[k .. '_range_maximum']
                elseif g_args[k] ~= nil then
                    value.min = g_args[k]
                    value.max = g_args[k]
                end
                if value.min == nil then
                else
                    temp_values[#temp_values+1] = {value=value, index=i}
                end
            end
        end
        
        local final_values = {}
        for i, data in ipairs(temp_values) do
            local opt = args.options[data.index]
            local v = data.value
            if opt.hide_default ~= nil and opt.hide_default == v.min and opt.hide_default == v.max then
            else
                table.insert(final_values, data)
            end
        end
        
        -- all zeros = dont display and return early
        if #final_values == 0 then
            return nil
        end
        
        local out = {}
        
        if args.before then
            out[#out+1] = h.new_color('default', args.before)
        end
        
        for i, data in ipairs(final_values) do
            local value = data.value
            value.base = base_values[data.index]
            
            local options = args.options[data.index]
            
            if options.color == nil and args.type == 'gem' then
                value.color = 'value'
            end
            
            out[#out+1] = h.format_value(value, options)
        end
        
        if args.after then
            out[#out+1] = h.new_color('default', args.after)
        end
        
        return table.concat(out, '')
    end
end

function core.factory.display_value_only(key)
    return function()
        return g_args[key]
    end
end

function core.factory.descriptor_value(args)
    -- Arguments:
    --  key
    --  tbl
    args = args or {}
    return function (value)
        args.tbl = args.tbl or g_args
        if args.tbl[args.key] then
            value = util.html.abbr(value, args.tbl[args.key])
        end
        return value
    end
end

--
-- argument mapping
--
-- format:
-- g_args key = {
--   no_copy = true or nil           -- When loading an base item, dont copy this key 
--   property = 'prop',              -- Property associated with this key
--   property_func = function or nil -- Function to unpack the property into a native lua value. 
--                                      If not specified, func is used. 
--                                      If neither is specified, value is copied as string
--   func = function or nil          -- Function to unpack the argument into a native lua value and validate it. 
--                                      If not specified, value will not be set.
--   default = object                -- Default value if the parameter is nil
-- }
core.map = {
    -- special params
    html = {
        no_copy = true,
        property = 'Has infobox HTML',
        func = nil,
    },
    implicit_stat_text = {
        property = 'Has implicit stat text',
        func = function ()
            g_args.implicit_stat_text = core.process_mod_stats{type='implicit'}
        end,
    },
    explicit_stat_text = {
        property = 'Has explicit stat text',
        func = function ()
            g_args.explicit_stat_text = core.process_mod_stats{type='explicit'}
            
            if g_args.is_talisman then
                g_args.explicit_stat_text = (g_args.explicit_stat_text or '') .. h.new_color('corrupted', 'Corrupted') 
            end
        end,
    },
    stat_text = {
        property = 'Has stat text',
        func = function()
            local sep = ''
            if g_args.implicit_stat_text and g_args.explicit_stat_text then
                sep = string.format('<span class="item-stat-separator -%s"></span>', g_args.frame_type)
            end
            local text = (g_args.implicit_stat_text or '') .. sep .. (g_args.explicit_stat_text or '')
            
            if string.len(text) > 0 then
                g_args.stat_text = text
            end
        end,
    },
    -- generic
    class = {
        no_copy = true,
        property = 'Has item class',
        func = core.factory.table_cast('class', {key='full', tbl=m_game.constants.item.class}),
    },
    rarity = {
        no_copy = true,
        property = 'Has rarity',
        func = core.factory.table_cast('rarity', {key={'full', 'long_lower'}, tbl=m_game.constants.item.rarity, rtrkey='full'}),
    },
    name = {
        no_copy = true,
        property = 'Has name',
        func = nil,
    },
    size_x = {
        property = 'Has inventory width',
        func = core.factory.number_cast('size_x'),
    },
    size_y = {
        property = 'Has inventory height',
        func = core.factory.number_cast('size_y'),
    },
    drop_enabled = {
        property = 'Is drop enabled',
        func = core.factory.boolean_cast('drop_enabled'),
        default = true,
    },
    drop_level = {
        no_copy = true,
        property = 'Has drop level',
        func = core.factory.number_cast('drop_level'),
    },
    drop_level_maximum = {
        no_copy = true,
        property = 'Has maximum drop level',
        func = core.factory.number_cast('drop_level_maximum'),
    },
    required_level = {
        property = 'Has base level requirement',
        func = core.factory.number_cast('required_level'),
        default = 1,
    },
    required_level_final = {
        property = 'Has level requirement',
        func = function ()
            g_args.required_level_final = g_args.required_level
        end,
        default = 1,
    },
    required_dexterity = {
        property = 'Has base dexterity requirement',
        func = core.factory.number_cast('required_dexterity'),
        default = 0,
    },
    required_strength = {
        property = 'Has base strength requirement',
        func = core.factory.number_cast('required_strength'),
        default = 0,
    },
    required_intelligence = {
        property = 'Has base intelligence requirement',
        func = core.factory.number_cast('required_intelligence'),
        default = 0,
    },
    inventory_icon = {
        no_copy = true,
        property = 'Has inventory icon',
        func = function ()
            g_args.inventory_icon_id = g_args.inventory_icon or g_args.name
            g_args.inventory_icon = string.format('File:%s inventory icon.png', g_args.inventory_icon_id) 
        end,
    },
    -- note: this must be called after inventory item to work correctly as it depends on g_args.inventory_icon_id being set
    alternate_art_inventory_icons = {
        no_copy = true,
        property = 'Has alternate inventory icons',
        func = function ()
            local icons = {}
            if g_args.alternate_art_inventory_icons ~= nil then
                local names = util.string.split(g_args.alternate_art_inventory_icons, ', ')
                
                for _, name in ipairs(names) do
                    icons[#icons+1] = string.format('File:%s %s inventory icon.png', g_args.inventory_icon_id, name) 
                end
            end
            g_args.alternate_art_inventory_icons = icons
        end,
        default = {},
    },
    help_text = {
        property = 'Has help text',
        func = nil,
    },
    flavour_text = {
        no_copy = true,
        property = 'Has flavour text',
        func = nil,
    },
    tags = {
        property = 'Has tags',
        property_func = function ()
            g_args.tags = util.string.split(g_args.tags, '<MANY>')
        end,
        func = core.factory.array_table_cast('tags', {
            tbl = m_game.constants.tags,
            errmsg = '%s is not a valid tag',
        }),
    },
    metadata_id = {
        no_copy = true,
        property = 'Has metadata id',
        func = nil,
    },
    release_version = {
        no_copy = true,
        property = 'Has release version',
        func = function ()
            if g_args.release_version ~= nil then
                g_args.release_version = util.cast.version(g_args.release_version, {return_type = 'string'})
            end
        end,
    },
    removal_version = {
        no_copy = true,
        property = 'Has release version',
        func = function ()
            if g_args.removal_version ~= nil then
                g_args.removal_version = util.cast.version(g_args.removal_version, {return_type = 'string'})
            end
        end,
    },
    --
    -- specific section
    --
    -- amulets
    is_talisman = {
        property = 'Is talisman',
        func = core.factory.boolean_cast('is_talisman'),
    },
    
    -- flasks
    charges_max = {
        property = 'Has base maximum flask charges',
        func = core.factory.number_cast('charges_max'),
    },
    
    charges_per_use = {
        property = 'Has base flask charges per use',
        func = core.factory.number_cast('charges_per_use'),
    },
    
    flask_mana = {
        property = 'Has base flask mana recovery',
        func = core.factory.number_cast('flask_mana'),
    },
    
    flask_life = {
        property = 'Has base flask life recovery',
        func = core.factory.number_cast('flask_life'),
    },
    
    flask_duration = {
        property = 'Has base flask duration',
        func = core.factory.number_cast('flask_duration'),
    },
    
    -- weapons & active skills
    critical_strike_chance = {
        property = 'Has base critical strike chance',
        func = core.factory.number_cast('critical_strike_chance'),
    },
    -- weapons
    attack_speed = {
        property = 'Has base attack speed',
        func = core.factory.number_cast('attack_speed'),
    },
    damage_min = {
        property = 'Has base minimum physical damage',
        func = core.factory.number_cast('damage_min'),
    },
    damage_max = {
        property = 'Has base maximum physical damage',
        func = core.factory.number_cast('damage_max'),
    },
    range = {
        property = 'Has base weapon range',
        func = core.factory.number_cast('range'),
    },
    -- armor-type stuff
    armour = {
        property = 'Has base armour',
        func = core.factory.number_cast('armour'),
        default = 0,
    },
    energy_shield = {
        property = 'Has base energy shield',
        func = core.factory.number_cast('energy_shield'),
        default = 0,
    },
    evasion = {
        property = 'Has base evasion',
        func = core.factory.number_cast('evasion'),
        default = 0,
    },
    -- shields
    block = {
        property = 'Has base block',
        func = core.factory.number_cast('block'),
    },
    -- skill gem stuff
    gem_description = {
        property = 'Has description',
        func = nil,
    },
    dexterity_percent = {
        property = 'Has dexterity percentage',
        func = core.factory.percentage('dexterity_percent'),
    },
    strength_percent = {
        property = 'Has strength percentage',
        func = core.factory.percentage('strength_percent'),
    },
    intelligence_percent = {
        property = 'Has intelligence percentage',
        func = core.factory.percentage('intelligence_percent'),
    },
    primary_attribute = {
        property = 'Has primary attribute',
        func = function()
            for _, attr in ipairs(m_game.constants.attributes) do
                local val = g_args[attr.long_lower .. '_percent'] 
                if val and val >= 60 then
                    g_args['primary_attribute'] = attr.long_upper
                    return
                end
            end
            g_args['primary_attribute'] = 'None'
        end,
    },
    gem_tags = {
        property = 'Has gem tags',
        -- TODO: default rework
        func = core.factory.array_table_cast('gem_tags', {
            tbl = m_game.constants.item.gem_tags,
            errmsg = '%s is not a valid tag',
        }),
        default = {},
    },
    experience = {
        property = 'Has maximum experience',
        func = core.factory.percentage('experience'),
    },
    skill_screenshot = {
        property = 'Has skill screenshot',
        func = function ()
            g_args.skill_screenshot = string.format('File:%s skill screenshot.jpg', g_args.name)
        end,
    },
    -- Active gems only
    gem_icon = {
        property = 'Has skill gem icon',
        func = function ()
            -- TODO readd support if needed.
            g_args.gem_icon = string.format('File:%s skill icon.png', g_args.name) 
        end,
    },
    -- Support gems only
    support_gem_letter_html = {
        property = 'Has support gem letter HTML',
        func = function ()
            if g_args.support_gem_letter == nil then
                return
            end
        
            -- TODO replace this with a loop possibly
            local css_map = {
                strength = 'red',
                intelligence = 'blue',
                dexterity = 'green',
            }
            local id
            for k, v in pairs(css_map) do
                k = string.format('%s_percent', k)
                if g_args[k] and g_args[k] > 50 then
                    id = v
                    break
                end
            end
            
            if id ~= nil then
                local container = mw.html.create('span')
                container
                    :attr('class', string.format('support-gem-id-%s', id))
                    :wikitext(g_args.support_gem_letter)
                    :done()
                g_args.support_gem_letter_html = tostring(container)
            end
        end,
    },
    -- Maps
    map_tier = {
        property = 'Has map tier',
        func = core.factory.number_cast('tier'),
    },
    map_guild_character = {
        property = 'Has map guild character',
        func = nil,
    },
    map_area_id = {
        property = 'Has map area id',
        func = nil, -- TODO: Validate against a query?
    },
    map_area_level = {
        property = 'Has map area level',
        func = core.factory.number_cast('map_area_level'),
    },
    unique_map_guild_character = {
        property = 'Has unqiue map guild character',
        func = nil,
    },
    unique_map_area_id = {
        property = 'Has unique map area id',
        func = nil, -- TODO: Validate against a query?
    },
    unique_map_area_level = {
        property = 'Has unique map area level',
        func = core.factory.number_cast('unique_map_area_level'),
    },
    --
    -- Currency-like items
    --
    stack_size = {
        property = 'Has stack size',
        func = core.factory.number_cast('stack_size'),
    },
    stack_size_currency_tab = {
        property = 'Has currency tab stack size',
        func = core.factory.number_cast('stack_size_currency_tab'),
    },
    description = {
        property = 'Has description',
        func = nil,
    },
    cosmetic_type = {
        property = 'Has cosmetic type',
        func = nil,
    },
    -- for essences
    is_essence = {
        property = 'Is essence',
        func = core.factory.boolean_cast('is_essence'),
        default = false,
    },
    essence_level_restriction = {
        property = 'Has essence level restriction',
        func = core.factory.number_cast('essence_level_restriction'),
    },
    essence_level = {
        property = 'Has essence level',
        func = core.factory.number_cast('essence_level'),
    },
    --
    -- hideout doodads (HideoutDoodads.dat)
    --
    is_master_doodad = {
        property = 'Is master doodad',
        func = core.factory.boolean_cast('is_master_doodad'),
    },
    master = {
        property = 'Is sold by master',
        -- todo validate against list of master names
        func = core.factory.table_cast('master', {key='full', tbl=m_game.constants.masters}),
    },
    master_level_requirement = {
        property = 'Has master level requirement',
        func = core.factory.number_cast('master_level_requirement'),
    },
    master_favour_cost = {
        property = 'Has master favour cost',
        func = core.factory.number_cast('master_favour_cost'),
    },
    variation_count = {
        property = 'Has variation count',
        func = core.factory.number_cast('variation_count'),
    },
    -- Propehcy
    prophecy_id = {
        property = 'Has prophecy id',
        func = nil,
    },
    prediction_text = {
        property = 'Has prophecy prediction text',
        func = nil,
    },
    seal_cost_normal = {
        property = 'Has prophecy seal cost in normal difficulty',
        func = core.factory.number_cast('seal_cost_normal'),
    },
    seal_cost_cruel = {
        property = 'Has prophecy seal cost in cruel difficulty',
        func = core.factory.number_cast('seal_cost_cruel'),
    },
    seal_cost_merciless = {
        property = 'Has prophecy seal cost in merciless difficulty',
        func = core.factory.number_cast('seal_cost_merciless'),
    },
    -- ------------------------------------------------------------------------
    -- derived stats
    -- ------------------------------------------------------------------------
    
    -- For rarity != normal, rarity already verified
    base_item = {
        no_copy = true,
        property = 'Has base item',
        func = function ()
            g_args.base_item = g_args.base_item_data['Has name']
        end,
    },
    base_item_id = {
        no_copy = true,
        property = 'Has base item metadata id',
        func = function()
            g_args.base_item_id = g_args.base_item_data['Has metadata id']
        end,
    },
    base_item_page = {
        no_copy = true,
        property = 'Has base item wiki page',
        func = function()
            g_args.base_item_page = g_args.base_item_data[1]
        end,
    },
    
    name_list = {
        no_copy = true,
        property = 'Has names',
        func = function ()
            if g_args.name_list ~= nil then
                g_args.name_list = util.string.split(g_args.name_list, ', ')
                g_args.name_list[#g_args.name_list+1] = g_args.name
            else
                g_args.name_list = {g_args.name}
            end
        end,
    },
    name_list_lower = {
        no_copy = true,
        property = 'Has lowercase names',
        func = function()
            g_args.name_list_lower = {}
            for index, value in ipairs(g_args.name_list) do
                g_args.name_list_lower[index] = string.lower(value)
            end
        end,
    },
    gem_tags_difference = {
        no_copy = true,
        property = 'Has gem tags difference',
        func = function()
            if g_args.gem_tags ~= nil then
                local gtags = {}
                -- copy tags
                for _, data in ipairs(m_game.constants.item.gem_tags) do
                    if data.full and data.full ~= '' then
                        gtags[data.full] = true
                    end
                end
                -- delete existing tags
                for _, tag in ipairs(g_args.gem_tags) do
                    gtags[tag] = nil
                end
                
                -- add them as ordered list and not as hash table so it is consistent with the other gem tag list
                g_args.gem_tags_difference = {}
                for key, value in pairs(gtags) do
                    g_args.gem_tags_difference[#g_args.gem_tags_difference+1] = key
                end
            end
        end,
    },
    release_date = {
        no_copy = true,
        property = 'Has release date',
        func = function ()
            if g_args.release_version == nil then
                return
            end
            
            local query = {}
            query[#query+1] = string.format('[[Is version::%s]]', g_args.release_version)
            query[#query+1] = '?Has release date#'
            
            local result = util.smw.query(query, g_frame)
            if #result == 0 then
                error(string.format('No results found for "%s" - version number has no version page or is invalid', g_args.release_version))
            elseif #result > 1 then
                error(string.format('Too many results found for "%s" - please check there is only 1 version page and no other pages transcluding it', g_args.release_version))
            end
            
            g_args.release_date = result[1]['Has release date'] 
        end,
    },
    frame_type = {
        no_copy = true,
        property = nil,
        func = function ()
            if g_args.name == 'Prophecy' or g_args.base_item == 'Prophecy' then
                g_args.frame_type = 'prophecy'
                return
            end
        
            local var = core.class_specifics[g_args.class]
            if var ~= nil and var.frame_type ~= nil then
                g_args.frame_type = var.frame_type
                return
            end
            
            g_args.frame_type = string.lower(g_args.rarity)
        end,
    },
    --
    -- args populated by mod validation
    -- 
    mods = {
        no_copy = true,
        property = 'Has mod ids',
        default = {},
    },
    implicit_mods = {
        property = 'Has implicit mod ids',
        property_func = function ()
            g_args.implicit_mods = util.string.split(g_args.implicit_mods, '<MANY>')
            for _, modid in ipairs(g_args.implicit_mods) do 
                g_args.mods[#g_args.mods+1] = modid
                g_args._mods[#g_args._mods+1] = {
                    result={},
                    modid=modid,
                    type='implicit',
                }
            end
        end,
        default = {},
    },
    explicit_mods = {
        no_copy = true,
        property = 'Has explicit mod ids',
        default = {},
    },
}

core.stat_map = {
    range = {
        property = 'Has weapon range',
        stats_add = {
            'local_weapon_range_+',
        },
    },
    damage_min = {
        property = 'Has minimum physical damage',
        stats_add = {
            'local_minimum_added_physical_damage',
        },
        stats_increased = {
            'local_physical_damage_+%',
        },
    },
    damage_max = {
        property = 'Has maximum physical damage',
        stats_add = {
            'local_maximum_added_physical_damage',
        },
        stats_increased = {
            'local_physical_damage_+%',
        },
    },
    fire_damage_min = {
        default = 0,
        property = 'Has minimum fire damage',
        stats_add = {
            'local_minimum_added_fire_damage',
        },
    },
    fire_damage_max = {
        default = 0,
        property = 'Has maximum fire damage',
        stats_add = {
            'local_maximum_added_fire_damage',
        },
    },
    cold_damage_min = {
        default = 0,
        property = 'Has minimum cold damage',
        stats_add = {
            'local_minimum_added_cold_damage',
        },
    },
    cold_damage_max = {
        default = 0,
        property = 'Has maximum cold damage',
        stats_add = {
            'local_maximum_added_cold_damage',
        },
    },
    lightning_damage_min = {
        default = 0,
        property = 'Has minimum lightning damage',
        stats_add = {
            'local_minimum_added_lightning_damage',
        },
    },
    lightning_damage_max = {
        default = 0,
        property = 'Has maximum lightning damage',
        stats_add = {
            'local_maximum_added_lightning_damage',
        },
    },
    chaos_damage_min = {
        default = 0,
        property = 'Has minimum chaos damage',
        stats_add = {
            'local_minimum_added_chaos_damage',
        },
    },
    chaos_damage_max = {
        default = 0,
        property = 'Has maximum chaos damage',
        stats_add = {
            'local_maximum_added_chaos_damage',
        },
    },
    critical_strike_chance = {
        property = 'Has critical strike chance',
        stats_add = {
            'local_critical_strike_chance',
        },
        stats_increased = {
            'local_critical_strike_chance_+%',
        },
    },
    attack_speed = {
        property = 'Has attack speed',
        stats_increased = {
            'local_attack_speed_+%',
        },
    },
    map_item_quantity = {
        property = 'Has map item quantity',
        stats_add = {
            'map_item_drop_quantity_+%',
        },
    },
    map_item_rarity = {
        property = 'Has map item rarity',
        stats_add = {
            'map_item_drop_rarity_+%',
        },
    },
    map_pack_size = {
        property = 'Has map pack size',
        stats_add = {
            'map_pack_size_+%',
        },
    },
    flask_life = {
        property = 'Has flask life recovery',
        stats_add = {
            'local_flask_life_to_recover',
        },
        stats_increased = {
            'local_flask_life_to_recover_+%',
        },
    },
    flask_mana = {
        property = 'Has flask mana recovery',
        stats_add = {
            'local_flask_mana_to_recover',
        },
        stats_increased = {
            'local_flask_mana_to_recover_+%',
        },
    },
    flask_duration = {
        property = 'Has flask duration',
        stats_increased = {
            'local_flask_recovery_speed_+%',
        },
    },
    charges_per_use = {
        property = 'Has flask charges per use',
        stats_increased = {
            'local_charges_used_+%',
        },
    },
    charges_max = {
        property = 'Has maximum flask charges',
        stats_add = {
            'local_extra_max_charges',
        },
        stats_increased = {
            'local_max_charges_+%',
            'local_charges_added_+%',
        },
    },
    block = {
        property = 'Has block',
        stats_add = {
            'local_additional_block_chance_%',
        },
    },
    armour = {
        property = 'Has armour',
        stats_add = {
            'local_base_physical_damage_reduction_rating',
        },
        stats_increased = {
            'local_physical_damage_reduction_rating_+%',
            'local_armour_and_energy_shield_+%',
            'local_armour_and_evasion_+%',
            'local_armour_and_evasion_and_energy_shield_+%',
        }
    },
    evasion = {
        property = 'Has evasion',
        stats_add = {
            'local_base_evasion_rating',
        },
        stats_increased = {
            'local_evasion_rating_+%',
            'local_evasion_and_energy_shield_+%',
            'local_armour_and_evasion_+%',
            'local_armour_and_evasion_and_energy_shield_+%',
        },
    },
    energy_shield = {
        property = 'Has energy shield',
        stats_add = {
            'local_energy_shield'
        },
        stats_increased = {
            'local_energy_shield_+%',
            'local_armour_and_energy_shield_+%',
            'local_evasion_and_energy_shield_+%',
            'local_armour_and_evasion_and_energy_shield_+%',
        },
    },
}

core.dps_map = {
    {
        name = 'physical_dps',
        property = 'physical damage per second',
        damage_args = {'damage', },
        label = util.html.abbr('pDPS', 'physical damage per second'),
    },
    {
        name = 'fire_dps',
        property = 'fire damage per second',
        damage_args = {'fire_damage'},
        label = util.html.abbr('Fire DPS', 'fire damage per second'),
    },
    {
        name = 'cold_dps',
        property = 'cold damage per second',
        damage_args = {'cold_damage'},
        label = util.html.abbr('Cold DPS', 'cold damage per second'),
    },
    {
        name = 'lightning_dps',
        property = 'lightning damage per second',
        damage_args = {'lightning_damage'},
        label = util.html.abbr('Light. DPS', 'lightning damage per second'),
    },
    {
        name = 'chaos_dps',
        property = 'chaos damage per second',
        damage_args = {'chaos_damage'},
        label = util.html.abbr('Chaos DPS', 'chaos damage per second'),
    },
    {
        name = 'elemental_dps',
        property = 'elemental damage per second',
        damage_args = {'fire_damage', 'cold_damage', 'lightning_damage'},
        label = util.html.abbr('eDPS', 'elemental damage (i.e. fire/cold/lightning) per second'),
    },
    {
        name = 'poison_dps',
        property = 'poison damage per second',
        damage_args = {'damage', 'chaos_damage'},
        label = util.html.abbr('Poison DPS', 'poison damage (i.e. physcial/chaos) per second'),
    },
    {
        name = 'dps',
        property = 'damage per second',
        damage_args = {'damage', 'fire_damage', 'cold_damage', 'lightning_damage', 'chaos_damage'},
        label = util.html.abbr('DPS', 'total damage (i.e. physcial/fire/cold/lightning/chaos) per second'),
    },
}

-- base item is default, but will be validated later
-- Notes:
--  inventory_icon must always be before alternate_art_inventory_icons 
core.default_args = {'class', 'rarity', 'name', 'name_list', 'name_list_lower', 'size_x', 'size_y', 'drop_enabled', 'drop_level', 'drop_level_maximum', 'required_level', 'required_level_final', 'inventory_icon', 'alternate_art_inventory_icons', 'flavour_text', 'help_text', 'tags', 'metadata_id', 'release_version', 'release_date', 'mods', 'implicit_mods', 'explicit_mods'}
-- frame_type is needed in stat_text
core.late_args = {'frame_type', 'implicit_stat_text', 'explicit_stat_text', 'stat_text'}
core.prophecy_args = {'prophecy_id', 'prediction_text', 'seal_cost_normal', 'seal_cost_cruel', 'seal_cost_merciless'}
core.class_groups = {
    flasks = {
        keys = {['Life Flasks'] = true, ['Mana Flasks'] = true, ['Hybrid Flasks'] = true, ['Utility Flasks'] = true, ['Critical Utility Flasks'] = true},
        args = {'flask_duration', 'charges_max', 'charges_per_use'},
    },
    weapons = {
        keys = {['Claws'] = true, ['Daggers'] = true, ['Wands'] = true, ['One Hand Swords'] = true, ['Thrusting One Hand Swords'] = true, ['One Hand Axes'] = true, ['One Hand Maces'] = true, ['Bows'] = true, ['Staves'] = true, ['Two Hand Swords'] = true, ['Two Hand Axes'] = true, ['Two Hand Maces'] = true, ['Sceptres'] = true, ['Fishing Rods'] = true},
        args = {'required_dexterity', 'required_intelligence', 'required_strength', 'critical_strike_chance', 'attack_speed', 'damage_min', 'damage_max', 'range'},
    },
    gems = {
        keys = {['Active Skill Gems'] = true, ['Support Skill Gems'] = true},
        args = {'dexterity_percent', 'strength_percent', 'intelligence_percent', 'primary_attribute', 'gem_tags', 'gem_tags_difference'},
    },
    armor = {
        keys = {['Gloves'] = true, ['Boots'] = true, ['Body Armours'] = true, ['Helmets'] = true, ['Shields'] = true},
        args = {'required_dexterity', 'required_intelligence', 'required_strength', 'armour', 'energy_shield', 'evasion'},
    },
    stackable = {
        keys = {['Currency'] = true, ['Stackable Currency'] = true, ['Hideout Doodads'] = true, ['Microtransactions'] = true, ['Divination Card'] = true},
        args = {'stack_size', 'stack_size_currency_tab', 'description', 'cosmetic_type'},
    },
}

core.class_specifics = {
    ['Amulets'] = {
        args = {'is_talisman'},
    },
    ['Life Flasks'] = {
        args = {'flask_life'},
    },
    ['Mana Flasks'] = {
        args = {'flask_mana'},
    },
    ['Hybrid Flasks'] = {
        args = {'flask_life', 'flask_mana'},
    },
    ['Active Skill Gems'] = {
        args = {'skill_screenshot', 'gem_icon'},
        defaults = {
            help_text = 'Place into an item socket of the right colour to gain this skill. Right click to remove from a socket.',
            size_x = 1,
            size_y = 1,
        },
        frame_type = 'gem',
    },
    ['Support Skill Gems'] = {
        args = {'support_gem_letter_html'},
        defaults = {
            help_text = 'This is a Support Gem. It does not grant a bonus to your character, but skills in sockets connected to it. Place into an item socket connected to a socket containing the Active Skill Gem you wish to augment. Right click to remove from a socket.',
            size_x = 1,
            size_y = 1,
        },
        frame_type = 'gem',
    },
    ['Shields'] = {
        args = {'block'},
    },
    ['Maps'] = {
        args = {'map_tier', 'map_guild_character', 'map_area_id', 'map_area_level', 'unique_map_area_id', 'unique_map_area_level', 'unique_map_guild_character'},
    },
    ['Currency'] = {
        frame_type = 'currency',
    },
    ['Stackable Currency'] = {
        args = {'is_essence', 'essence_level_restriction', 'essence_level'},
        frame_type = 'currency',
    },
    ['Microtransactions'] = {
        frame_type = 'currency',
    },
    ['Hideout Doodads'] = {
        args = {'is_master_doodad', 'master', 'master_level_requirement', 'master_favour_cost', 'variation_count'},
        defaults = {
            help_text = 'Right click on this item then left click on a location on the ground to create the object.',
        },
        frame_type = 'currency',
    },
    ['Jewel'] = {
        defaults = {
            help_text = 'Place into an allocated Jewel Socket on the Passive Skill Tree. Right click to remove from the Socket.',
        },
        skip_stat_lines = {
            'Limited to %d+ %(Hidden%)',
            'Jewel has a radius of %d+ %(Hidden%)',
        },
    },
    ['Quest Items'] = {
        frame_type = 'quest',
    },
    ['Divination Card'] = {
        frame_type = 'divicard',
    },
}

-- add defaults from class specifics and class groups
core.item_classes = {}
for _, data in ipairs(m_game.constants.item.class) do
    core.item_classes[data['full']] = {
        args = xtable:new(),
        defaults = {},
    }
end

for _, row in pairs(core.class_groups) do
    for k, _ in pairs(row.keys) do
        core.item_classes[k].args:insertT(row.args)
    end
end


for k, row in pairs(core.class_specifics) do
    if row.args ~= nil then
        core.item_classes[k].args:insertT(row.args)
    end
    if row.defaults ~= nil then
        for key, value in pairs(row.defaults) do
            core.item_classes[k].defaults[key] = value
        end
   end
end


-- GroupTable -> RowTable -> formatter function 
--
--

core.display_groups = {
    -- Tags, stats, level, etc
    {
        {
            args = function ()
                if g_args.class == nil then 
                    return false 
                end
                
                return core.class_groups.weapons.keys[g_args.class] ~= nil
            end,
            func = core.factory.display_value{
                keys={'class'},
                options = {
                    [1] = {
                        color = 'default',
                    },
                },
            },
        },
        {
            args = {'gem_tags'},
            func = function ()
                local out = {}
                for i, tag in ipairs(g_args.gem_tags) do
                    out[#out+1] = string.format('[[:Category:%s (gem tag)|%s]]', tag, tag)
                end
                
                return table.concat(out, ', ')
            end,
        },
        {
            args = {'support_gem_letter_html'},
            func = core.factory.display_value{
                keys={'support_gem_letter_html'},
                options = {
                    [1] = {
                        before = 'Icon: ',
                    },
                },
            },
        },
        {
            args = {'radius'},
            func = core.factory.display_value{
                keys={'radius', 'radius_secondary', 'radius_tertiary'},
                options = {
                    [1] = {
                        before = 'Radius: ',
                        func = core.factory.descriptor_value{key='radius_description'},
                    },
                    [2] = {
                        before = ' / ',
                        func = core.factory.descriptor_value{key='radius_secondary_description'},
                    },
                    [3] = {
                        before = ' / ',
                        func = core.factory.descriptor_value{key='radius_tertiary_description'},
                    },
                },
            },
        },
        -- TODO: gem level here. Maybe put max level here?
        {
            args = nil,
            func = core.factory.display_value{
                keys={'mana_cost'},
                type='gem',
                options = {
                    [1] = {
                        hide_default = 100,
                        fmt = function ()
                            if g_args.has_percentage_mana_cost then
                                return '%i%%'
                            else
                                return '%i'
                            end
                        end,
                        before = function ()
                            if g_args.has_reservation_mana_cost then
                                return 'Mana Reserved: '
                            else
                                return 'Mana Cost: '
                            end
                        end,
                    },
                },
            },
        },
        {
            args = nil,
            func = core.factory.display_value{
                keys={'mana_multiplier'},
                type='gem',
                options = {
                    [1] = {
                        hide_default = 100,
                        fmt = '%i%%',
                        before = 'Mana Multiplier: ',
                    },
                },
            },
        },
        {
            args = nil,
            func = core.factory.display_value{
                keys={'vaal_souls_requirement', 'vaal_souls_requirement', 'vaal_souls_requirement'},
                type='gem',
                options = {
                    [1] = {
                        hide_default = 0,
                        fmt = '%i (N) / ',
                        before = 'Souls per use: ',
                    },
                    [2] = {
                        hide_default = 0,
                        fmt = '%i (C) / ',
                        func = function (value)
                            return value*1.5
                        end,
                    },
                    [3] = {
                        hide_default = 0,
                        fmt = '%i (M)',
                        func = function (value)
                            return value*2
                        end,
                    },
                },
            },
        },
        {
            args = nil,
            func = core.factory.display_value{
                keys={'vaal_stored_uses'},
                type='gem',
                options = {
                    [1] = {
                        hide_default = 0,
                        fmt = '%i',
                        before = 'Can store ',
                        after = ' use(s)',
                    },
                },
            },
        },
        {
            args = nil,
            func = core.factory.display_value{
                keys={'stored_uses'},
                type='gem',
                options = {
                    [1] = {
                        hide_default = 0,
                        fmt = '%i',
                        before = 'Can store ',
                        after = ' use(s)',
                    },
                },
            },
        },
        {
            args = nil,
            func = core.factory.display_value{
                keys={'cooldown'},
                type='gem',
                options = {
                    [1] = {
                        hide_default = 0,
                        fmt = '%.2f sec',
                        before = 'Cooldown Time: ',
                    },
                },
            },
        },
        {
            args = {'cast_time'},
            func = core.factory.display_value{
                keys={'cast_time'},
                options = {
                    [1] = {
                        hide_default = 0,
                        fmt = '%.2f sec',
                        before = 'Cast Time: ',
                    },
                },
            },
        },
        {
            args = nil,
            func = core.factory.display_value{
                keys={'critical_strike_chance'},
                type='gem',
                options = {
                    [1] = {
                        hide_default = 0,
                        fmt = '%.2f%%',
                        before = 'Critical Strike Chance: ',
                    },
                },
            },
        },
        {
            args = nil,
            func = core.factory.display_value{
                type='gem',
                keys={'damage_effectiveness'},
                options = {
                    [1] = {
                        hide_default = 100,
                        fmt = '%i%%',
                        before = 'Damage Effectiveness: ',
                    },
                },
            },
        },
        {
            args = {'projectile_speed'},
            func = core.factory.display_value{
                keys={'projectile_speed'},
                options = {
                    [1] = {
                        before = 'Projectile Speed: ',
                    },
                },
            },
        },
        -- Weapon only
        {
            args = {'damage_min', 'damage_max'},
            func = core.factory.display_value{
                keys={'damage_min', 'damage_max'},
                options = {
                    [1] = {
                        fmt = '%i',
                        after = '&ndash;',
                        before = 'Physical Damage: '
                    },
                    [2] = {
                        fmt = '%i',
                    },
                },
            },       
        },
        {
            args = nil,
            func = core.factory.display_value{
                before='Elemental Damage: ',
                keys = {'fire_damage_min', 'fire_damage_max', 'cold_damage_min', 'cold_damage_max', 'lightning_damage_min', 'lightning_damage_max'},
                options = {
                    [1] = {
                        hide_default = 0,
                        fmt = '%i',
                        after = h.new_color('fire', '&ndash;'),
                        color = 'fire',
                    },
                    [2] = {
                        hide_default = 0,
                        fmt = '%i',
                        after = ' ',
                        color = 'fire',
                    },
                    [3] = {
                        hide_default = 0,
                        fmt = '%i',
                        after = h.new_color('cold', '&ndash;'),
                        color = 'cold',
                    },
                    [4] = {
                        hide_default = 0,
                        fmt = '%i',
                        after = ' ',
                        color = 'cold',
                    },
                    [5] = {
                        hide_default = 0,
                        fmt = '%i',
                        after = h.new_color('lightning', '&ndash;'),
                        color = 'lightning',
                    },
                    [6] = {
                        hide_default = 0,
                        fmt = '%i',
                        color = 'lightning',
                    },
                },
            },       
        },
        {
            args = nil,
            func = core.factory.display_value{
                before='Chaos Damage: ',
                keys = {'chaos_damage_min', 'chaos_damage_max'},
                options = {
                    [1] = {
                        hide_default = 0,
                        fmt = '%i',
                        after = h.new_color('chaos', '&ndash;'),
                        color = 'chaos',
                    },
                    [2] = {
                        hide_default = 0,
                        fmt='%i',
                        color = 'chaos',
                    },
                },
            },       
        },
        {
            args = {'critical_strike_chance'},
            func = core.factory.display_value{
                keys={'critical_strike_chance'},
                options = {
                    [1] = {
                        fmt = '%.2f%%',
                        before = 'Critical Strike Chance: ',
                    },
                },
            },
        },
        {
            args = {'attack_speed'},
            func = core.factory.display_value{
                keys={'attack_speed'},
                options = {
                    [1] = {
                        fmt = '%.2f',
                        before = 'Attacks per Second: ',
                    },
                },
            },
        },
        -- Map only
        {
            args = {'map_area_level'},
            func = core.factory.display_value{
                keys={'map_area_level'},
                options = {
                    [1] = {
                        fmt = '%i',
                        before = 'Map Level: ',
                    },
                },
            },
        },
        {
            args = {'map_tier'},
            func = core.factory.display_value{
                keys={'map_tier'},
                options = {
                    [1] = {
                        fmt = '%i',
                        before = 'Map Tier: ',
                    },
                },
            },
        },
        {
            args = {'map_guild_character'},
            func = core.factory.display_value{
                keys={'map_guild_character'},
                options = {
                    [1] = {
                        fmt = '%s',
                        before = util.html.abbr('Guild Character', 'When used in guild creation, this map can be used for the listed character') .. ': ',
                    },
                },
            },
        },
        --[[{
            args = {'map_item_quantity'},
            func = core.factory.display_value{
                keys={'map_item_quantity'},
                options = {
                    [1] = {
                        fmt = '%i',
                        before = 'Item Quantity: ',
                    },
                },
            },
        },
        {
            args = {'map_item_rarity'},
            func = core.factory.display_value{
                keys={'map_item_rarity'},
                options = {
                    [1] = {
                        fmt = '%i',
                        before = 'Item Rarity: ',
                    },
                },
            },
        },
        {
            args = {'map_pack_size'},
            func = core.factory.display_value{
                keys={'map_pack_size'},
                options = {
                    [1] = {
                        fmt = '%i',
                        before = 'Pack Size: ',
                    },
                },
            },
        },]]--
        -- Jewel Only
        {
            args = {'limit'},
            func = core.factory.display_value{
                keys={'limit'},
                options = {
                    [1] = {
                        fmt = '%i',
                        before = 'Limit: ',
                    },
                },
            },
        },
        {
            args = {'jewel_radius'},
            func = core.factory.display_value{
                keys={'jewel_radius'},
                options = {
                    [1] = {
                        fmt = '%i',
                        before = 'Radius: ',
                    },
                },
            },
        },
        -- Flask only
        {
            args = {'flask_mana', 'flask_duration'},
            --func = core.factory.display_flask('flask_mana'),
            func = core.factory.display_value{
                keys = {'flask_mana', 'flask_duration'},
                options = {
                    [1] = {
                        fmt = '%i',
                        before = 'Recovers ',
                    },
                    [2] = {
                        fmt = '%.2f',
                        before = ' Mana over ',
                        after = ' seconds',
                    },
                }
            },
        },
        {
            args = {'flask_life', 'flask_duration'},
            func = core.factory.display_value{
                keys = {'flask_life', 'flask_duration'},
                options = {
                    [1] = {
                        fmt = '%i',
                        before = 'Recovers ',
                    },
                    [2] = {
                        fmt = '%.2f',
                        before = ' Life over ',
                        after = ' seconds',
                    },
                }
            },
        },
        {
            -- don't display for mana/life flasks
            args = function ()
                for _, k in ipairs({'flask_life', 'flask_mana'}) do
                    if g_args[k] ~= nil then
                        return false
                    end
                end
                
                return g_args['flask_duration'] ~= nil
            end,
            func = core.factory.display_value{
                keys = {'flask_duration'},
                options = {
                    [1] = {
                        before = 'Lasts ',
                        after = ' Seconds',
                        fmt = '%.2f',
                    },
                },
            },
        },
        {
            args = {'charges_per_use', 'charges_max'},
            func = core.factory.display_value{
                keys = {'charges_per_use', 'charges_max'},
                options = {
                    [1] = {
                        before = 'Consumes ',
                        fmt = '%i',
                    },
                    [2] = {
                        before = ' of ',
                        after = ' Charges on use',
                        fmt = '%i',
                    },
                },
            },
        },
        -- armor
        {
            args = {'block'},
            func = core.factory.display_value{
                keys={'block'},
                options = {
                    [1] = {
                        before = 'Block: ',
                        fmt = '%i%%',
                        hide_default = 0,
                    },
                },
            },
        },
        {
            args = {'armour'},
            func = core.factory.display_value{
                keys={'armour'},
                options = {
                    [1] = {
                        before = 'Armour: ',
                        fmt = '%i',
                        hide_default = 0,
                    },
                },
            },
        },
        {
            args = {'evasion'},
            func = core.factory.display_value{
                keys={'evasion'},
                options = {
                    [1] = {
                        before = 'Evasion: ',
                        fmt = '%i',
                        hide_default = 0,
                    },
                },
            },
        },
        {
            args = {'energy_shield'},
            func = core.factory.display_value{
                keys={'energy_shield'},
                options = {
                    [1] = {
                        before = 'Energy Shield: ',
                        fmt = '%i',
                        hide_default = 0,
                    },
                },
            },
        },
        -- Misc
        {
            args = {'stack_size'},
            func = core.factory.display_value{
                keys={'stack_size'},
                options = {
                    [1] = {
                        hide_default = 1,
                        fmt = '%i',
                        before = 'Stack Size: ',
                    },
                },
            },
        },
        -- Jewel stat based info
        {
            args = nil,
            func = core.factory.display_value{
                type = 'stat',
                keys = {'local_unique_item_limit'},
                options = {
                    [1] = {
                        fmt = '%i',
                        before = 'Limited to: ',
                    },
                },
            },
        },
        {
            args = nil,
            func = core.factory.display_value{
                type = 'stat',
                keys = {'local_jewel_effect_base_radius'},
                options = {
                    [1] = {
                        fmt = '%s',
                        func = function(value)
                            return string.format('%s (%i)', (m_game.constants.item.jewel_radius_to_size[value] or '?'), value) 
                        end,
                        before = 'Radius: ',
                    },
                },
            },
        },
        -- Essence stuff
        {
            args = {'essence_level'},
            func = core.factory.display_value{
                keys={'essence_level'},
                options = {
                    [1] = {
                        fmt = '%i',
                        before = 'Essence Level: ',
                    },
                },
            },
        },
    },
    -- Requirements
    {
        {
            args = {'master', 'master_level_requirement'},
            func = function()
                -- masters have been validated before
                local data
                for i, rowdata in ipairs(m_game.constants.masters) do
                    if g_args.master == rowdata.full then
                        data = rowdata
                        break
                    end
                end
                
                return h.new_color('default', 'Requires ') .. string.format('[[%s|%s %s]]', data.full, data.short_upper, g_args.master_level_requirement)
            end
        },
        {
            args = function ()
                if g_args.drop_enabled == true then
                    return false
                end
                return true
            end,
            func = function()
                local span = mw.html.create('span')
                span
                    :attr('class', 'infobox-disabled-drop')
                    :wikitext('DROP DISABLED')
                    :done()
                return tostring(span)
            end,
        },
        -- Instead of item level, show drop level if any
        {
            args = {'drop_enabled'},
            func = core.factory.display_value{
                keys={'drop_level', 'drop_level_maximum'},
                options = {
                    [1] = {
                        fmt = '%i',
                        before = 'Drop Level: ',
                    },
                    [2] = {
                        fmt = '%i',
                        before = ' / ',
                    },
                },
            },
        },
        {
            args = nil,
            func = function ()
                local requirements = {}
                local attr_label
                local use_short_label
                
                if g_args.required_level_final and g_args.required_level_final > 1 then
                    table.insert( requirements, 'Level ' .. tostring( h.new_color('value', g_args.required_level_final) ) )
                end
                
                for _, attr in ipairs(m_game.constants.attributes) do
                    local val = g_args['required_' .. attr['long_lower']]
                    if val and val > 0 then
                        use_short_label = false or g_args.required_level_final
                        for _, attr2 in ipairs(m_game.constants.attributes) do
                            if attr ~= attr2 then
                                use_short_label = use_short_label or g_args[attr2['long_lower']]
                            end
                        end
                        if use_short_label then
                            attr_label = attr['short_upper']
                        else
                            attr_label = attr['long_upper']
                        end
                        table.insert( requirements, h.new_color('value', val) .. ' ' .. attr_label )
                    end
                end
                
                -- return early
                if #requirements == 0 then
                    return
                end
                
                return h.new_color('default', 'Requires ') .. table.concat(requirements, ', ')
            end,
        },
    },
    -- Gem description
    {
        css_class = '-textwrap text-color -gemdesc',
        {
            args = {'gem_description'},
            func = core.factory.display_value_only('gem_description'),
        },
    },
    -- Gem Quality Stats
    {
        css_class = '-textwrap text-color -mod',
        {
            args = {'quality_stat_text'},
            func = function ()
                lines = {}
                lines[#lines+1] = h.new_color('default', 'Per 1% Quality:')
                lines[#lines+1] = g_args.quality_stat_text
                
                return table.concat(lines, '<br>')
            end,
        },
    },
    -- Gem Implicit Stats
    {
        css_class = '-textwrap text-color -mod',
        {
            args = function ()
                return core.class_groups.gems.keys[g_args.class] and g_args.stat_text
            end,
            func = function ()
                lines = {}
                lines[#lines+1] = g_args.stat_text
                if g_args.gem_tags:contains('Vaal') then
                    lines[#lines+1] = h.new_color('corrupted', 'Corrupted') 
                end
                return table.concat(lines, '<br>')
            end,
        },
    },
    -- Implicit Stats
    {
        css_class = 'text-color -mod',
        func = function ()
            if g_args.implicit_stat_text ~= '' then
                return {g_args.implicit_stat_text}
            else
                return {}
            end
        end,
    },
    -- Stats
    {
        css_class = 'text-color -mod',
        func = function ()
            if g_args.explicit_stat_text ~= '' then
                return {g_args.explicit_stat_text}
            else
                return {}
            end
        end,
    },
    -- Experience
    {
        {
            args = {'experience'},
            func = core.factory.display_value{
                keys={'experience'},
                options = {
                    [1] = {
                        fmt = '%i',
                    },
                },
            },
        },
    },
    -- Description (currency, doodads)
    {
        css_class = '-textwrap text-color -mod',
        {
            args = {'description'},
            func = core.factory.display_value_only('description'),
        },
    },
    -- Variations (for doodads)
    {
       css_class = 'text-color -mod',
        {
            args = {'variation_count'},
            func = function ()
                local txt
                if g_args.variation_count == 1 then
                    txt = 'Variation'
                else
                    txt = 'Variations'
                end
                return string.format('%i %s', g_args.variation_count, txt)
            end,
        },
    },
    -- Flavour Text
    {
        css_class = '-textwrap text-color -flavour',
        {
            args = {'flavour_text'},
            func = core.factory.display_value_only('flavour_text'),
        },
    },
    -- Prophecy text
    {
        css_class = '-textwrap text-color -value',
        {
            args = {'prediction_text'},
            func = core.factory.display_value_only('prediction_text'),
        },
    },
    -- Help text
    {
        css_class = '-textwrap text-color -help',
        {
            args = {'help_text'},
            func = core.factory.display_value_only('help_text'),
        },
    },
    -- Cost (i.e. vendor costs)
    {
        --css_class = '',
        {
            args = {'master_favour_cost'},
            func = core.factory.display_value{
                keys={'master_favour_cost'},
                options = {
                    [1] = {
                        before = 'Favour cost: ',
                        color = 'currency',
                    },
                },
            },
        },
        {
            args = {'seal_cost_normal', 'seal_cost_cruel', 'seal_cost_merciless'},
            func = core.factory.display_value{
                keys={'seal_cost_normal', 'seal_cost_cruel', 'seal_cost_merciless'},
                options = {
                    [1] = {
                        before = 'Seal cost: <br>',
                        fmt = '%dx/',
                        color = 'currency',
                    },
                    [2] = {
                        fmt = '%dx/',
                        color = 'currency',
                    },
                    [3] = {
                        fmt = '%dx',
                        color = 'currency',
                        after = function () 
                            -- direct call will mess up g_args
                            return g_frame:expandTemplate{title='Item link',args={item_name_exact='Silver Coin'}}
                        end,
                    },
                },
            },
        },
    },
}

core.item_link_params = {'name', 'inventory_icon', 'html'}
core.item_link_broken_cat = '[[Category:Pages with broken item links]]'

core.result = {}

core.result.generic_item = {
    {
        arg = 'drop_level',
        header = 'Drop<br>Level',
        property = 'Has drop level',
        cast = tonumber,
        display = h.tbl.display.na_or_val,
    },
    {
        arg = 'stack_size',
        header = 'Stack<br>Size',
        property = 'Has stack size',
        cast = tonumber,
        display = h.tbl.display.na_or_val,
    },
    {
        arg = 'stack_size_currency_tab',
        header = util.html.abbr('Tab<br>Stack<br>Size', 'Stack size in the currency stash tab'),
        property = 'Has currency tab stack size',
        cast = tonumber,
        display = h.tbl.display.na_or_val,
    },
    {
        arg = 'level',
        header = m_game.level_requirement.icon,
        property = 'Has level requirement',
        cast = tonumber,
        display = h.tbl.display.na_or_val,
    },
    {
        arg = 'ar',
        header = util.html.abbr('AR', 'Armour'),
        property = 'Has armour range average',
        cast = nil,
        display = h.tbl.display.factory.range{property='armour'},
    },
    {
        arg = 'ev',
        header = util.html.abbr('EV', 'Evasion Rating'),
        property = 'Has evasion range average',
        cast = nil,
        display = h.tbl.display.factory.range{property='evasion'},
    },
    {
        arg = 'es',
        header = util.html.abbr('ES', 'Energy Shield'),
        property = 'Has energy shield range average',
        cast = nil,
        display = h.tbl.display.factory.range{property='energy shield'},
    },
    {
        arg = 'block',
        header = 'Block',
        property = 'Has block range average',
        cast = nil,
        display = h.tbl.display.factory.range{property='block'},
    },
    -- Todo: replace this with a proper range eventually.
    {
        arg = 'weapon',
        header = 'Min<br>Damage',
        property = 'Has minimum physical damage range average',
        cast = nil,
        display = h.tbl.display.factory.range{property='minimum physical damage'},
        
    },
    {
        arg = 'weapon',
        header = 'Max<br>Damage',
        property = 'Has maximum physical damage range average',
        cast = nil,
        display = h.tbl.display.factory.range{property='maximum physical damage'},
        
    },
    {
        arg = 'weapon',
        header = util.html.abbr('APS', 'Attacks per second'),
        property = 'Has attack speed range average',
        cast = nil,
        display = h.tbl.display.factory.range{property='attack speed'},
        
    },
    {
        arg = 'weapon',
        header = 'Critical',
        property = 'Has critical strike chance range average',
        cast = nil,
        display = h.tbl.display.factory.range{property='critical strike chance'},
        
    },
    {
        arg = 'flask_life',
        header = util.html.abbr('Life', 'Life regenerated over the flask duration'),
        property = 'Has flask life recovery range average',
        cast = nil,
        display = h.tbl.display.factory.range{property='flask life recovery'},
    },
    {
        arg = 'flask_mana',
        header = util.html.abbr('Mana', 'Mana regenerated over the flask duration'),
        property = 'Has flask mana recovery range average',
        cast = nil,
        display = h.tbl.display.factory.range{property='flask mana recovery'},
    },
    {
        arg = 'flask',
        header = 'Duration',
        property = 'Has flask duration range average',
        cast = nil,
        display = h.tbl.display.factory.range{property='flask duration'},
    },
    {
        arg = 'flask',
        header = util.html.abbr('Usage', 'Number of charges consumed on use'),
        property = 'Has flask charges per use range average',
        cast = nil,
        display = h.tbl.display.factory.range{property='flask charges per use'},
    },
    {
        arg = 'flask',
        header = util.html.abbr('Capacity', 'Maximum number of flask charges held'),
        property = 'Has maximum flask charges range average',
        cast = nil,
        display = h.tbl.display.factory.range{property='maximum flask charges'},
    },
    {
        arg = 'stat',
        header = 'Stats',
        property = 'Has stat text',
        cast = nil,
        display = function (tr, value, data) 
            return h.na_or_val(tr, value, function (value)
                return h.new_color('mod', value)
            end)
        end,
    },
    {
        arg = 'description',
        header = 'Effect(s)',
        property = 'Has description',
        cast = nil,
        display = function (tr, value, data) 
            return h.na_or_val(tr, value, function (value)
                return h.new_color('mod', value)
            end)
        end,
    },
    {
        arg = 'flavour_text',
        header = 'Flavour Text',
        property = 'Has flavour text',
        cast = nil,
        display = function (tr, value, data) 
            return h.na_or_val(tr, value, function (value)
                return h.new_color('flavour', value)
            end)
        end,
    },
    {
        arg = 'help_text',
        header = 'Help Text',
        property = 'Has help text',
        cast = nil,
        display = function (tr, value, data) 
            return h.na_or_val(tr, value, function (value)
                return h.new_color('help', value)
            end)
        end,
    },
}

for _, data in ipairs(core.dps_map) do
    table.insert(core.result.generic_item, #core.result.generic_item-4, {
        arg = data.name,
        header = data.label,
        property = nil,
        cast = nil,
        display = h.tbl.display.factory.range{property=data.property, no_base=true},
    })
end

core.result.skill_gem = {
    {
        arg = 'icon',
        header = util.html.abbr('L', 'Support gem letter.'),
        property = 'Has support gem letter HTML',
        cast = nil,
        display = h.tbl.display.na_or_val,
    },
    {
        arg = 'skill_icon',
        header = 'Icon',
        property = 'Has skill gem icon',
        cast = nil,
        display = h.tbl.display.wikilink,
    },
    {
        arg = 'description',
        header = 'Description',
        property = 'Has description',
        cast = nil,
        display = h.tbl.display.na_or_val,
    },
    {
        arg = 'level',
        header = m_game.level_requirement.icon,
        property = 'Has level requirement',
        cast = tonumber,
        display = h.tbl.display.na_or_val,
    },
    {
        arg = 'crit',
        header = util.html.abbr('Crit', 'Critical Strike Chance'),
        property = 'Has critical strike chance',
        cast = tonumber,
        display = h.tbl.display.percent,
    },
    {
        arg = 'cast_time',
        header = util.html.abbr('Cast<br>Time', 'Casting time of the skill in seconds'),
        property = 'Has cast time',
        cast = tonumber,
        display = h.tbl.display.seconds,
    },
    {
        arg = 'dmgeff',
        header = util.html.abbr('Dmg.<br>Eff.', 'Damage Effectiveness'),
        property = 'Has damage effectiveness',
        cast = tonumber,
        display = h.tbl.display.percent,
    },
    {
        arg = 'mcm',
        header = util.html.abbr('MCM', 'Mana cost multiplier - missing values indicate it changes on gem level'),
        property = 'Has mana multiplier',
        cast = tonumber,
        display = h.tbl.display.percent,
    },
    {
        arg = 'mana',
        header = util.html.abbr('Mana', 'Mana cost'),
        property = 'Has mana cost',
        cast = tonumber,
        display = function (tr, value, data)
            local appendix = ''
            if data['?Has percentage mana cost'] then
                appendix = appendix .. '%'
            end
            if data['?Has reservation mana cost'] then
                appendix = appendix .. ' ' .. util.html.abbr('R', 'reserves mana')
            end
            
            local str
            
            if value ~= nil then
                str = string.format('%d', value) .. appendix
            end 
            return h.na_or_val(tr, str)
        end,
    },
    {
        arg = 'vaal',
        header = util.html.abbr('Souls', 'Vaal souls requirement in Normal/Cruel/Merciless difficulty'),
        property = 'Has vaal souls requirement',
        cast = tonumber,
        display = function (tr, value, data) 
            return h.na_or_val(tr, value, function (value)
                return string.format('%d / %d / %d', value, value*1.5, value*2)
            end)
        end,
    },
    {
        arg = 'vaal',
        header = util.html.abbr('Uses', 'Maximum number of stored uses'),
        property = 'Has vaal stored uses',
        cast = tonumber,
        display = h.tbl.display.na_or_val,
    },
    {
        arg = 'radius',
        header = util.html.abbr('R1', 'Primary radius'),
        property = 'Has primary radius',
        cast = tonumber,
        display = function (tr, value, data)
            return h.na_or_val(tr, value, core.factory.descriptor_value{key='?Has primary radius description', tbl=data})
        end,
    },
    {
        arg = 'radius',
        header = util.html.abbr('R2', 'Secondary radius'),
        property = 'Has secondary radius',
        cast = tonumber,
        display = function (tr, value, data)
            return h.na_or_val(tr, value, core.factory.descriptor_value{key='?Has secondary radius description', tbl=data})
        end,
    },
    {
        arg = 'radius',
        header = util.html.abbr('R3', 'Tertiary radius'),
        property = 'Has tertiary radius',
        cast = tonumber,
        display = function (tr, value, data)
            return h.na_or_val(tr, value, core.factory.descriptor_value{key='?Has tertiary radius description', tbl=data})
        end,
    },
}

for _, attr in ipairs(m_game.constants.attributes) do
    table.insert(core.result.generic_item, 1, {
        arg = attr.short_lower,
        header = attr.icon,
        property = string.format('Has base %s requirement', attr.long_lower),
        cast = tonumber,
        display = h.tbl.display.na_or_val,
    })
    table.insert(core.result.skill_gem, 3, {
        arg = attr.short_lower,
        header = attr.icon,
        property = string.format('Has %s percentage', attr.long_lower),
        cast = tonumber,
        display = function (tr, value) 
            return h.na_or_val(tr, value, function (value)
                return '[[File:Yes.png|yes|link=]]'
            end)
        end,
    })
end

-- ----------------------------------------------------------------------------
-- Page views
-- ----------------------------------------------------------------------------

--
-- Template:Item
--

function p.itembox (frame)
    --
    -- Args/Frame
    --
    local t = os.clock()
    
    g_args = getArgs(frame, {
        parentFirst = true
    })
    g_frame = util.misc.get_frame(frame)
    
    --
    -- Shared args
    --
    
    g_args._total_args = {}
    g_args._base_item_args = {}
    g_args._mods = {}
    g_args._stats = {}
    g_args._subobjects = {}
    g_args._properties = {}
    
    -- Must validate some argument early. It is required for future things
    local err
    
    err = core.process_arguments{array=core.default_args}
    if err then
        return err
    end
    
    err = core.process_arguments{array=core.item_classes[g_args.class].args}
    if err then
        return err
    end
    
    -- set defaults
    
    for k, v in pairs(core.item_classes[g_args.class].defaults) do
        if g_args[k] == nil then
            g_args[k] = v
        end
    end
    
    -- Base Item

    core.process_base_item()
    
    -- Prophecy special snowflake
    if g_args.base_item == 'Prophecy' then
        err = core.process_arguments{array=core.prophecy_args}
        if err then
            return err
        end
        
        g_args.inventory_icon = 'File:Prophecy inventory icon.png'
    end
    
    -- Mods

    for _, k in ipairs({'implicit', 'explicit'}) do
        local success = true
        local i = 1
        while success do
            success = core.validate_mod{key=k, i=i}
            i = i + 1
        end
    end
    
    core.process_smw_mods()
        
    -- Add stats - this is for when mods are not set, but we still need stats to calcuate new armour values etc
    util.args.stats(g_args, {prefix='extra_'})
    for _, stat in ipairs(g_args.extra_stats) do
        if stat.value ~= nil then
            stat.min = stat.value
            stat.max = stat.value
            stat.avg = stat.value
        end
        h.stats_update(stat.id, stat)
    end

    -- Transpose stats into subobjects
    for id, data in pairs(g_args._stats) do
        g_args._subobjects[id] = {
            ['Has stat ids'] = id,
            ['Has minimum stat values'] = data.min,
            ['Has maximum stat values'] = data.max,
            ['Has average stat values'] = data.avg,
        }
    end
    
    -- Handle extra stats (for gems)
    
    if core.class_groups.gems.keys[g_args.class] then
        m_skill.skill(g_frame, g_args)
    end
    
    for k, data in pairs(core.stat_map) do
        local value = g_args[k]
 
        if value == nil and data.default ~= nil then
            value = data.default
            g_args[k] = data.default
        end
        
        if value ~= nil then
            value = {min=value, max=value}
            -- The simple cases; this must be using ipairs as "add" must apply before
            for _, operator in ipairs({'add', 'more'}) do
                local st = data['stats_' .. operator]
                if st ~= nil then
                    for _, statid in ipairs(st) do
                        if g_args._stats[statid] ~= nil then
                            h.stat[operator](value, g_args._stats[statid])
                        end
                    end
                end
            end
            
            -- For increased stats we need to add them up first
            local st = data.stats_increased
            if st ~= nil then
                local total_increase = {min=0, max=0}
                for _, statid in ipairs(st) do
                    if g_args._stats[statid] ~= nil then
                        for var, current_value in pairs(total_increase) do
                            total_increase[var] = current_value + g_args._stats[statid][var]
                        end
                    end
                end
                h.stat.more(value, total_increase)
            end
            
            value.avg = (value.min + value.max) / 2
            
            -- don't add the properties unless we need to
            if (data.default ~= nil and (value.min ~= data.default or value.max ~= data.default)) or data.default == nil then
                for short_key, range_data in pairs(h.range_map) do
                    g_args._properties[data.property .. range_data.property] = value[short_key]
                end
            end
            
            for short_key, range_data in pairs(h.range_map) do
                g_args[k .. range_data.var] = value[short_key]
            end
        end
    end
    
    -- late processing
    err = core.process_arguments{array=core.late_args}
    if err then
        return err
    end
    
    -- calculate and handle weapon dps
    if core.class_groups.weapons.keys[g_args.class] then
        for _, data in ipairs(core.dps_map) do
            local damage = {
                min = {},
                max = {},
            }
        
            for var_type, value in pairs(damage) do
                -- covers the min/max/avg range
                for short_key, range_data in pairs(h.range_map) do
                    value[short_key] = 0
                    for _, damage_key in ipairs(data.damage_args) do
                        value[short_key] = value[short_key] + g_args[string.format('%s_%s%s', damage_key, var_type, range_data.var)]
                    end
                end
            end

            for short_key, range_data in pairs(h.range_map) do
                local result = (damage.min[short_key] + damage.max[short_key]) / 2 * g_args[string.format('attack_speed%s', range_data.var)]
                g_args[string.format('%s%s', data.name, range_data.var)] = result
                -- It will set the property, even if 0. 
                -- Not sure if it is better to not set it, but on the other hand this way it can be queried for having no dps of a particular type
                g_args._properties[string.format('Has %s%s', data.property, range_data.property)] = result
            end
        end
    end
    
    
    -- Setting semantic properties Part 1 (base values)
    
    local val
    
    for _, k in ipairs(g_args._total_args) do
        local prop = core.map[k].property
        val = g_args[k]
        if val == nil then
        elseif prop == nil then
            --mw.logObject(k)
        else
            g_args._properties[prop] = val
        end
    end
    
    util.smw.set(g_frame, g_args._properties)
    
    -- Subobjects
    local command
    for key, properties in pairs(g_args._subobjects) do
        command = ''
        if type(key) ~= 'number' then
            command = key
        end
        util.smw.subobject(g_frame, command, properties)
    end
    
    --
    -- Validate arguments
    -- 
    
    local x = v.itembox()
    
    mw.logObject(os.clock() - t)
    
    return x
end

--
-- Template:Item link & Template:Sl
--

function p.item_link (frame)
    --
    -- Args/Frame
    --
    
    g_args = getArgs(frame, {
        parentFirst = true,
        removeBlanks = false,
    })
    g_frame = util.misc.get_frame(frame)
    
    -- Backwards compability
    g_args.item_name = g_args.item_name or g_args[1]
    if g_args.item_name ~= nil then
        g_args.item_name = string.lower(g_args.item_name)
    end
    g_args.name = g_args.name or g_args[2]
    
    if util.table.has_all_value(g_args, {'page', 'item_name', 'item_name_exact'}) then
        error('page, item_name or item_name_exact must be specified')
    end
    
    g_args.large = util.cast.boolean(g_args.large)
    
    local img
    local result
    
    if util.table.has_one_value(g_args, core.item_link_params, nil) or g_args.item_name ~= nil then
        local query = {}
        
        if g_args.page ~= nil then
            -- TODO returns the result even if the + format is specified. 
            query[#query+1] = string.format('[[%s]]', g_args.page)
        else
            if g_args.item_name ~= nil then
                query[#query+1] = string.format('[[Has lowercase names::%s]]', g_args.item_name)
            elseif g_args.item_name_exact ~= nil then
                query[#query+1] = string.format('[[Has name::%s]]', g_args.item_name_exact)
            end
            
            query[#query] = query[#query] .. ' [[Has inventory icon::+]] [[Has infobox HTML::+]]'
            
            if g_args.link_type == 'skill' then
                query[#query] = query[#query] .. ' [[Concept:Skill gems]]'
            end
        end
        
        query[#query+1] = '?Has name'
        query[#query+1] = '?Has inventory icon'
        query[#query+1] = '?Has infobox HTML'
        query[#query+1] = '?Has alternate inventory icons'
        query[#query+1] = '?Has inventory width'
        query[#query+1] = '?Has inventory height'
        
        -- attributes
        result = util.smw.query(query, g_frame)
        
        local err
        if #result == 0 then
            err = util.misc.raise_error_or_return{raise_required=true, args=g_args, msg=string.format(
                'No results found for search parameter "%s".', 
                g_args.page or g_args.item_name or g_args.item_name_exact 
            )}
        elseif #result > 1 then
            err = util.misc.raise_error_or_return{raise_required=true, args=g_args, msg=string.format(
                'Too many results for search parameter "%s". Consider using page parameter instead.',
                g_args.page or g_args.item_name or g_args.item_name_exact
            )}
        end
        
        if err ~= nil then
            return err .. core.item_link_broken_cat
        end
        
        result = result[1]
    else
        result = {g_args.page or g_args.item_name_exact}
    end
    
    for _, k in ipairs(core.item_link_params) do
        local prop = core.map[k].property
        if g_args[k] ~= nil then
            result[prop] = g_args[k]
        end
    end
    
    if g_args.image ~= nil then
        if result['Has alternate inventory icons'] == '' then
            return util.misc.raise_error_or_return{raise_required=true, args=g_args, msg=string.format(
                'Image parameter was specified, but there is no alternate art defined on page "%s"',
                result[1]
            ) .. core.item_link_broken_cat}
        end
        
        result['Has alternate inventory icons'] = util.string.split(result['Has alternate inventory icons'], '<MANY>')
        
        local index = tonumber(g_args.image)
        if index ~= nil then
            img = result['Has alternate inventory icons'][index]
        else
            -- offset 1 is needed
            local suffix = string.len(' inventory icon.png') + 1 
            -- add an extra offset by 1 to account for the space 
            local prefix = string.len(string.sub(result['Has inventory icon'], 1, -suffix)) + 2
            
            for _, filename in ipairs(result['Has alternate inventory icons']) do
                if string.sub(filename, prefix, -suffix) == g_args.image then
                    img = filename
                    break
                end
            end
        end
        
        if img == nil then
            return util.misc.raise_error_or_return{raise_required=true, args=g_args, msg=string.format(
                'Alternate art with index/name "%s" not found on page "%s"',
                g_args.image, result[1]
            ) .. core.item_link_broken_cat}
        end
    elseif result['Has inventory icon'] ~= '' then
        img = result['Has inventory icon']
    end
    
    -- output
    
    local container = mw.html.create('span')
    container:attr('class', 'inline-infobox-container')
    
    if not g_args.large then
        container:wikitext(string.format('[[%s]]', img))
    end
       
    container:wikitext(string.format('[[%s|%s]]', result[1], result['Has name'] or result[1]))
        
    if result['Has infobox HTML'] ~= '' then
        container
            :tag('span')
                :attr('class', 'inline-infobox-hover')
                :wikitext(result['Has infobox HTML'])
                :wikitext(string.format('[[%s]]', img))
                :done()
    end
    
    if g_args.large then
        local width = tonumber(result['Has inventory width']) or tonumber(g_args.width)
        local height = tonumber(result['Has inventory height']) or tonumber(g_args.height)
        
        if width and height then
            img = string.format('[[%s|%sx%spx]]', img, width*image_size, height*image_size)
        elseif width then
            img = string.format('[[%s|%spx]]', img, width*image_size)
        elseif height then
            img = string.format('[[%s|x%spx]]', img, height*image_size)
        else
            img = string.format('[[%s]]', img)
        end
    
        container:wikitext(img)
    end
        
    return tostring(container)
end

--
-- Template:Il
--

function p.item_link_compability (frame)
    if util.misc.is_frame(frame) then
        frame.args.raise = true
    else
        frame.raise = true
    end
    
    local result

    local status, err = pcall(function () 
        result = p.item_link(frame) 
    end)
    mw.logObject(err)
    
    if status then
        return result
    elseif string.match(err, ".*Too many results.*") then
        return err .. core.item_link_broken_cat
    else
        return require('Module:Item').itemLink(frame)
    end
end


-- ----------------------------------------------------------------------------
-- Result formatting templates for SMW queries
-- ----------------------------------------------------------------------------

--
-- Template:
--

function p.simple_item_list_row(frame)
    -- Args
    g_args = getArgs(frame, {
        parentFirst = true
    })
    g_frame = util.misc.get_frame(frame)
    
    --
    local args = util.string.split_args(g_args.userparam, {sep=', '})
    g_args.userparam = args
    
    local link = p.item_link{page=g_args[1], name=g_args['?Has name'], inventory_icon=g_args['?Has inventory icon'] or '', html=g_args['?Has infobox HTML'] or ''}
    if args.format == nil then
        return '* ' .. link
    elseif args.format == 'none' then
        return link
    elseif args.format == 'li' then
        return string.format('<li>%s</li>', link)
    else
        error(string.format('Unrecognized format parameter "%s"', args.format))
    end
end

-- ----------------------------------------------------------------------------
-- Reponsibile for subtemplates of Template:SMW item table
-- 
item_table_factory = {}
function item_table_factory.intro(args)
    -- args:
    --  data_array
    --  header
    
    return function (frame)
        -- Args
        local tpl_args = getArgs(frame, {
            parentFirst = true
        })
        g_frame = util.misc.get_frame(frame)
        
        --
        tpl_args.userparam = util.string.split_args(tpl_args.userparam, {sep=', '})
        
        local tr = mw.html.create('tr')
        tr
            :tag('th')
                :wikitext(args.header)
                :done()
                
        for _, rowinfo in ipairs(args.data_array) do
            if rowinfo.arg == nil or tpl_args.userparam[rowinfo.arg] then
                tr
                    :tag('th')
                        :wikitext(rowinfo.header)
                        :done()
            end
        end
        
        return '<table class="wikitable sortable">' .. tostring(tr)
    end
end

function item_table_factory.row(args)
    -- args:
    --  data_array
    return function (frame)
        -- Args
        local tpl_args = getArgs(frame, {
            parentFirst = true
        })
        g_frame = util.misc.get_frame(frame)
        
        --
        tpl_args.userparam = util.string.split_args(tpl_args.userparam, {sep=', '})
        
        local tr = mw.html.create('tr')
        
        local il_args = {
            page=tpl_args[1], 
            name=tpl_args['?Has name'], 
            inventory_icon=tpl_args['?Has inventory icon'], 
            html=tpl_args['?Has infobox HTML'],
            width=tpl_args['?Has inventory width'],
            height=tpl_args['?Has inventory height'],
        }
        if tpl_args.userparam.large then
            il_args.large = tpl_args.userparam.large
        end
        
        tr
            :tag('td')
                :wikitext(p.item_link(il_args))
                :done()
                
        for _, rowinfo in ipairs(args.data_array) do
            if rowinfo.arg == nil or tpl_args.userparam[rowinfo.arg] then
                local value
                if rowinfo.property ~= nil then
                    value = tpl_args['?' .. rowinfo.property]
                    if rowinfo.cast then
                        value = rowinfo.cast(value)
                    end
                end
                rowinfo.display(tr, value, tpl_args)
            end
        end
        
        return tostring(tr)
    end
end

-- Template:SMW item table/skill_gem/intro
p.skill_gem_list_intro = item_table_factory.intro{data_array=core.result.skill_gem, header='Skill gem'}
-- Template:SMW item table/skill gem
p.skill_gem_list_row = item_table_factory.row{data_array=core.result.skill_gem}

-- Template:SMW item table/intro
p.item_list_intro = item_table_factory.intro{data_array=core.result.generic_item, header='Item'}
-- Template:SMW item table
p.item_list_row = item_table_factory.row{data_array=core.result.generic_item}

-- ----------------------------------------------------------------------------
-- Item lists
-- ----------------------------------------------------------------------------

function p.skill_gem_list_by_gem_tag(frame)
    -- Args
    g_args = getArgs(frame, {
        parentFirst = true
    })
    g_frame = util.misc.get_frame(frame)
    
    if g_args.class == 'Support Skill Gems' then
    elseif g_args.class == 'Active Skill Gems' then
    else
        error('invalid item class')
    end

    local query = {}
    query[#query+1] = string.format('[[Has item class::%s]]', g_args.class)
    query[#query+1] = '?Has gem tags'
    query[#query+1] = '?Has name'
    query[#query+1] = '?Has inventory icon'
    --query[#query+1] = '?Has infobox HTML'
    query.limit = 5000
    query.sort = 'Has name'
    
    local results = util.smw.query(query, g_frame)
    
    local tags = {}
    
    for _, row in ipairs(results) do
        row['Has gem tags'] = util.string.split(row['Has gem tags'], '<MANY>')
        for _, tag in ipairs(row['Has gem tags']) do
            if tags[tag] == nil then
                tags[tag] = {}
            end
            table.insert(tags[tag], row)
        end
    end
    
    local tags_sorted = {}
    for tag, _ in pairs(tags) do
        table.insert(tags_sorted, tag)
    end
    table.sort(tags_sorted)
    
    local tbl = mw.html.create('table')
    tbl
        :attr('class', 'wikitable sortable')
        :tag('tr')
            :tag('th')
                :wikitext('Tag')
                :done()
            :tag('th')
                :wikitext('Skills')
                :done()
            :done()
    
    for _, tag in ipairs(tags_sorted) do
        local rows = tags[tag]
        local tr = tbl:tag('tr')
        tr
            :tag('td')
                :wikitext(tag)
            
        local td = tr:tag('td')
        for i, row in ipairs(rows) do
            td:wikitext(p.item_link{page=row[1], name=row['Has Name'], inventory_icon=row['Has inventory icon'], html=row['Has infobox HTML'] or ''})
            if i < #rows then
                td:wikitext('<br>')
            end
        end
    end
   
    return tostring(tbl)
end

-- ----------------------------------------------------------------------------
-- Misc. Item templates
-- ----------------------------------------------------------------------------

-- 
-- Template:Item class
--

function p.item_class (frame)
    -- Get args
    g_args = getArgs(frame, {
        parentFirst = true
    })
    g_frame = util.misc.get_frame(frame)
	
	if not doInfoCard then
		doInfoCard = require('Module:Infocard')._main
	end
    
    core.factory.table_cast('name', {key='full', tbl=m_game.constants.item.class})()
    
    if g_args.name_list ~= nil then
        g_args.name_list = util.string.split(g_args.name_list, ', ')
    else
        g_args.name_list = {}
    end
    
    --
    
    local ul = mw.html.create('ul')
    for _, item in ipairs(g_args.name_list) do
        ul
            :tag('li')
                :wikitext(item)
                :done()
    end
    

    -- Output Infocard
    
    local tplargs = {
        ['header'] = g_args.name,
        ['subheader'] = '[[Item class]] ' .. util.html.abbr('(?)', 'Item classes categorize items. Classes are often used to restrict items or skill gems to a specific class or by item filters'),
        [1] = 'Also reffered to as:' .. tostring(ul),
    }
    
    -- cats
    
    local cats = {
        'Item classes', 
    }
    
    -- Done
    
    return doInfoCard(tplargs) .. util.misc.add_category(cats)
end

-- ----------------------------------------------------------------------------
-- Property views..
-- ----------------------------------------------------------------------------

function v.itembox ()
    local container = v._itembox_core()
    
    if g_args.gem_icon ~= nil then
        container:wikitext(string.format('[[%s]]', g_args.gem_icon))
    end
    
    -- Store the infobox so it can be accessed with ease on other pages
    g_frame:callParserFunction('#set:', {['Has infobox HTML'] = tostring(container)})
    
    if g_args.inventory_icon ~= nil then
        container:wikitext(string.format('[[%s]]', g_args.inventory_icon))
    end
    
    local infobox = tostring(container)
    
    local span = mw.html.create('span')
    span
        :attr('class', 'infobox-page-container')
        :wikitext(infobox)
        
    if g_args.skill_screenshot then
        span:wikitext(string.format('<br>[[%s|300px]]', g_args.skill_screenshot))
    end
    
    return tostring(span) .. v._itembox_categories()
end

function v._itembox_core()
    -- needed later for setting caculated properties
    g_args._properties = {}
    local container = mw.html.create('span')
        :attr( 'class', 'item-box -' .. g_args.frame_type)
    
    if g_args.class == 'Divination Card' then
        --TODO div card code
    else
        local header_css
        if g_args.base_item and g_args.rarity ~= 'Normal' then
            line_type = 'double'
        else
            line_type = 'single'
        end
        
        local name_line = g_args.name
        if g_args.base_item and g_args.base_item ~= 'Prophecy' then
            name_line = name_line .. '<br>' .. g_args.base_item
        end
        
        container
            :tag('span')
                :attr( 'class', 'header -' .. line_type )
				:wikitext( name_line )
            :done()
            
        local grpcont
        local valid
        local statcont = container:tag('span')
        statcont
            :attr('class', 'item-stats')
            :done()
            
        for _, group in ipairs(core.display_groups) do
            grpcont = {}
            if group.func == nil then
                for _, disp in ipairs(group) do
                    valid = true
                    -- No args to verify which means always valid
                    if disp.args == nil then
                    elseif type(disp.args) == 'table' then
                        for _, key in ipairs(disp.args) do
                            if g_args[key] == nil then
                                valid = false
                                break
                            end
                        end
                    elseif type(disp.args) == 'function' then
                        valid = disp.args()
                    end
                    if valid then
                        grpcont[#grpcont+1] = disp.func()
                    end
                end
            else
                grpcont = group.func()
            end
            
            if #grpcont > 0 then
                statcont
                    :tag('span')
                    :attr('class', 'group ' .. (group.css_class or ''))
                    :wikitext(table.concat(grpcont, '<br>'))
                    :done()
            end
        end
    end
    
    g_frame:callParserFunction('#set:', g_args._properties)
    
    return container
end

function v._itembox_categories()
    cats = {}
    if g_args.rarity == 'Unique' then
        cats[#cats+1] = 'Unique ' .. g_args.class
    elseif g_args.base_item == 'Prophecy' then
        cats[#cats+1] = 'Prophecies'
    else
        cats[#cats+1] = g_args.class
    end
    
    for _, attr in ipairs(m_game.constants.attributes) do
        if g_args[attr.long_lower .. '_percent'] then
            cats[#cats+1] = string.format('%s %s', attr.long_upper, g_args.class)
        end
    end
    
    local suffix
    if g_args.class == 'Active Skill Gems' or g_args.class == 'Support Skill Gems' then
        suffix = ' (gem tag)'
    end
    if suffix ~= nil then
        for _, tag in ipairs(g_args.gem_tags) do
            cats[#cats+1] = tag .. suffix
        end
    end
    
    if #g_args.alternate_art_inventory_icons > 0 then
        cats[#cats+1] = 'Items with alternate artwork'
    end
    
    -- TODO: add maintenance categories
    
    if g_args.release_version == nil then
        cats[#cats+1] = 'Items without a release version'
    end
    
    --
    -- Output formatting
    --
    
    return util.misc.add_category(cats, {ingore_blacklist=g_args.debug})
end

return p