Module:Area: Difference between revisions

From Path of Exile 2 Wiki
Jump to navigation Jump to search
>OmegaK2
(Working around cargo bugs, yay?)
m (84 revisions imported)
 
(41 intermediate revisions by 5 users not shown)
Line 1: Line 1:
-- SMW powered area module
-------------------------------------------------------------------------------
--
--                            Module:Area
--
-- This module implements Template:Area and Template:Query area infoboxes
-------------------------------------------------------------------------------


-- ----------------------------------------------------------------------------
-- ----------------------------------------------------------------------------
Line 7: Line 12:
-- spawn_weight* values
-- spawn_weight* values
-- spawnchacne values
-- spawnchacne values
--
-- i18n for properties


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


local m_util = require('Module:Util')
local m_game = mw.loadData('Module:Game')
local getArgs = require('Module:Arguments').getArgs
local m_game = require('Module:Game')
local f_infocard = require('Module:Infocard')._main


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


-- ----------------------------------------------------------------------------
local f_infocard = require('Module:Infocard')._main
-- Localization
-- ----------------------------------------------------------------------------


-- Strings
-- The cfg table contains all localisable strings and configuration, to make it
local i18n = {
-- easier to port this module to another wiki.
    images = {
local cfg = use_sandbox and mw.loadData('Module:Area/config/sandbox') or mw.loadData('Module:Area/config')
        waypoint_no = '[[File:No waypoint area icon.png|link=|No Waypoint]]',
        waypoint_yes = '[[File:Waypoint area icon.png|link=|No Waypoint]]',
        waypoint_town = '[[File:Town area icon.png|link=|Town Hub]]',
        loading_screen = 'File:%s loading screen.png',
        loading_screen_infobox = '[[File:%s loading screen.png|250px]]',
        screenshot = 'File:%s area screenshot.%s',
        screenshot_infobox = '[[File:%s area screenshot.%s|250px]]',
    },


    args = {
local i18n = cfg.i18n
        main_page = 'main_page',
        id = 'id',
        name = 'name',
        act = 'act',
        area_level = 'level',
        level_restriction_max = 'level_restriction_max',
        area_type_tags = 'area_type_tags',
        tags = 'tags',
        loading_screen = 'loading_screen',
        connection_ids = 'connection_ids',
        parent_area_id = 'parent_area_id',
        modifier_ids = 'modifier_ids',
        boss_monster_ids = 'boss_monster_ids',
        monster_ids = 'monster_ids',
        entry_text = 'entry_text',
        entry_npc = 'entry_npc',
        flavour_text = 'flavour_text',
        screenshot_ext = 'screenshot_ext',
        vaal_area_spawn_chance = 'vaal_area_spawn_chance',
        vaal_area_ids = 'vaal_area_ids',
        strongbox_spawn_chance = 'strongbox_spawn_chance',
        strongbox_max_count = 'strongbox_max',
        strongbox_rarity_weight = 'strongbox_rarity_weight',
        is_map_area = 'is_map_area',
        is_unique_map_area = 'is_unique_map_area',
        is_town_area = 'is_town_area',
        is_hideout_area = 'is_hideout_area',
        is_vaal_area = 'is_vaal_area',
        is_master_daily_area = 'is_master_daily_area',
        is_labyrinth_area = 'is_labyrinth_area',
        is_labyrinth_airlock_area = 'is_labyrinth_airlock_area',
        is_labyrinth_boss_area = 'is_labyrinth_boss_area',
        has_waypoint = 'has_waypoint',
    },
    errors = {
        invalid_tag = '%s is not a valid tag',
        main_page_is_invalid = 'main_page argument got "%s" which is not a valid wiki page',
        main_page_does_not_exist = 'main_page argument requires the specified page "%s" to exist in the main wiki namespace',
    },
    tooltips = {
        -- boolean tooltips. Use singular form here, categories makes these plural.
        is_map_area = 'Map area',
        is_unique_map_area = 'Unique Map area',
        is_town_area = 'Town area',
        is_hideout_area = 'Hideout area',
        is_vaal_area = 'Vaal area',
        is_master_daily_area = 'Master daily spawn area',
        is_labyrinth_area = 'Labyrinth area',
        is_labyrinth_airlock_area = 'Labyrinth airlock area',
        is_labyrinth_boss_area = 'Labyrinth boss area',
        area = 'area',
       
        --
        entry_message = '%s: %s',
    },
    headers = {
        id = 'Id',
        act = 'Act',
        area_level = 'Area level',
        level_restriction_max = m_util.html.abbr('Max level', 'Characters above this level can not enter this zone.'),
        area_type_tags = 'Area type tags',
        tags = 'Tags',
        parent_area = m_util.html.abbr('Parent Town', 'Portals will lead to the parent town'),
        connections = 'Connections',
        -- monsters
        -- boss monsters
        vaal_areas = 'Vaal Areas',
    },
}


-- ----------------------------------------------------------------------------
-- ----------------------------------------------------------------------------
-- Constats
-- Utility & helper functions
-- ----------------------------------------------------------------------------
-- ----------------------------------------------------------------------------


local c = {}
local h = {}
-- TODO: test
c.max_area_query = 8


-- ----------------------------------------------------------------------------
--
-- Utility & helper functions
-- Functions for processing tpl_args
-- ----------------------------------------------------------------------------
--
h.proc = {}
h.proc.factory = {}


local factory = {}
function h.proc.factory.list(args)
function factory.arg_list(k, args)
    args = args or {}
     return function (tpl_args, frame)
     return function (tpl_args, value)
         if tpl_args[k] ~= nil then
         return m_util.cast.table(value, {
             tpl_args[k] = m_util.string.split(tpl_args[k], ', ')
            pattern = args.pattern,
         end
             callback = args.callback,
         })
     end
     end
end
end
local factory = {}


function factory.display_value(k, args)
function factory.display_value(k, args)
     return function(tpl_args, frame)
     return function(tpl_args)
         return tpl_args[k]
         return tpl_args[k]
     end
     end
Line 137: Line 62:
local util = {}
local util = {}
util.display = {}
util.display = {}
function util.display.multiple_areas(tpl_args, frame, area_ids)
function util.display.multiple_areas(tpl_args, area_ids)
     local out = {}
     local out = {}
     for _, area_id in ipairs(area_ids) do
     for _, area_id in ipairs(area_ids) do
         out[#out+1] = util.display.single_area(tpl_args, frame, area_id)
         out[#out+1] = util.display.single_area(tpl_args, area_id)
     end
     end
     return table.concat(out, '<br>')
     return table.concat(out, '<br>')
end
end


function util.display.single_area(tpl_args, frame, area_id)
function util.display.single_area(tpl_args, area_id)
     if tpl_args.areas[area_id] then
     if tpl_args.areas[area_id] then
         if tpl_args.areas[area_id]['areas.main_page'] ~= '' then
         if tpl_args.areas[area_id]['areas.main_page'] then
             return string.format('[[%s|%s]]', tpl_args.areas[area_id]['areas.main_page'], tpl_args.areas[area_id]['areas.name'])
             return string.format('[[%s|%s]]', tpl_args.areas[area_id]['areas.main_page'], tpl_args.areas[area_id]['areas.name'])
         else
         else
Line 160: Line 85:
-- Argument & display mapping
-- Argument & display mapping
-- ----------------------------------------------------------------------------
-- ----------------------------------------------------------------------------
local display  = {}


local argument_map = {
local argument_map = {
     table = 'areas',
     table = 'areas',
   
    order = {
        'main_page',
        'id',
        'name',
        'act',
        'area_level',
        'level_restriction_max',
        'area_type_tags',
        'tags',
        'loading_screen',
        'connection_ids',
        'parent_area_id',
        'modifier_ids',
        -- populated via modifier ids
        'stat_text',
        'boss_monster_ids',
        'monster_ids',
        'entry_text',
        'entry_npc',
        'flavour_text',
        'screenshot_ext',
        'screenshot',
        'vaal_area_spawn_chance',
        'vaal_area_ids',
        'strongbox_spawn_chance',
        'strongbox_max_count',
        'strongbox_rarity_weight',
        -- those four are parsed via the argument above
        'strongbox_weight_normal',
        'strongbox_weight_magic',
        'strongbox_weight_rare',
        'strongbox_weight_unique',
        'is_map_area',
        'is_unique_map_area',
        'is_legacy_map_area',
        'is_town_area',
        'is_hideout_area',
        'is_vaal_area',
        'is_labyrinth_area',
        'is_labyrinth_airlock_area',
        'is_labyrinth_boss_area',
        'has_waypoint',
        'mainpage_categories',
       
        -- Non argument, but passed to the script
        'areas',
    },
   
     --
     --
     -- User supplied arguments
     -- User supplied arguments
Line 170: Line 145:
             field = 'main_page',  
             field = 'main_page',  
             type = 'Page',
             type = 'Page',
             func = function(tpl_args, frame)
             func = function(tpl_args, value)
                local page = tpl_args.main_page
                 if value ~= nil then
                 if page ~= nil then
                     local page = mw.title.new(value)
                     page = mw.title.new(tpl_args.main_page)
                     if page == nil then
                     if page == nil then
                         error(string.format(i18n.errors.main_page_is_invalid, tpl_args.main_page))
                         error(string.format(i18n.errors.main_page_is_invalid, value))
                     elseif not page.exists then
                     elseif not page.exists then
                         error(string.format(i18n.errors.main_page_does_not_exist, tpl_args.main_page))
                         error(string.format(i18n.errors.main_page_does_not_exist, value))
                     else
                     else
                         -- dont need the title object anymore
                         -- dont need the title object anymore
Line 183: Line 157:
                     end
                     end
                 end
                 end
                return value
             end
             end
         },
         },
Line 202: Line 177:
             field = 'act',
             field = 'act',
             type = 'Integer',
             type = 'Integer',
            func = m_util.cast.factory.number(i18n.args.act, {key_out='act'}),
         },
         },
         area_level = {
         area_level = {
             field = 'area_level',
             field = 'area_level',
             type = 'Integer',
             type = 'Integer',
            func = m_util.cast.factory.number(i18n.args.area_level, {key_out='area_level'}),
         },
         },
         level_restriction_max = {
         level_restriction_max = {
             field = 'level_restriction_max',
             field = 'level_restriction_max',
             type = 'Integer',
             type = 'Integer',
            func = m_util.cast.factory.number(i18n.args.level_restriction_max, {key_out='level_restriction_max'}),
             default = 100,
             default = 100,
         },
         },
Line 218: Line 190:
             field = 'area_type_tags',
             field = 'area_type_tags',
             type = 'List (,) of String',
             type = 'List (,) of String',
             func = m_util.cast.factory.assoc_table(i18n.args.area_type_tags, {
             func = h.proc.factory.list{
                tbl = m_game.constants.tags,
                callback = m_util.validate.factory.in_table_keys{
                errmsg = i18n.errors.invalid_tag,
                    tbl = m_game.constants.tags,
                 key_out = 'area_type_tags',
                    errmsg = i18n.errors.invalid_tag,
             }),
                    errlvl = 4,
                 },
            },
             default = {},
         },
         },
         tags = {
         tags = {
             field = 'tags',
             field = 'tags',
             type = 'List (,) of String',
             type = 'List (,) of String',
             func = m_util.cast.factory.assoc_table(i18n.args.tags, {
             func = h.proc.factory.list{
                tbl = m_game.constants.tags,
                callback = m_util.validate.factory.in_table_keys{
                errmsg = i18n.errors.invalid_tag,
                    tbl = m_game.constants.tags,
                 key_out = 'tags',
                    errmsg = i18n.errors.invalid_tag,
             }),
                    errlvl = 4,
                 },
            },
             default = {},
         },
         },
         loading_screen = {
         loading_screen = {
             field = 'loading_screen',
             field = 'loading_screen',
             type = 'Page',
             type = 'Page',
             func = function (tpl_args, frame)
             func = function (tpl_args, value)
                local loading_id = tpl_args[i18n.args.loading_screen]
                 if value ~= nil then
                 if loading_id ~= nil then
                    -- value contains loading id
                     tpl_args.loading_screen = string.format(i18n.images.loading_screen, loading_id)
                     tpl_args.loading_screen_infobox = string.format(i18n.images.loading_screen_infobox, value)
                     tpl_args.loading_screen_infobox = string.format(i18n.images.loading_screen_infobox, loading_id)  
                      
                    return string.format(i18n.images.loading_screen, value)
                 end
                 end
             end,
             end,
Line 247: Line 226:
             field = 'connection_ids',
             field = 'connection_ids',
             type = 'List (,) of String',
             type = 'List (,) of String',
            func = factory.arg_list(i18n.args.connection_ids, {key_out='connection_ids'}),
             default = {},
             default = {},
         },
         },
Line 257: Line 235:
             field = 'modifier_ids',
             field = 'modifier_ids',
             type = 'List (,) of String',
             type = 'List (,) of String',
             func = function(tpl_args, frame)
             func = function(tpl_args, value)
                factory.arg_list(i18n.args.modifier_ids, {key_out='modifier_ids'})(tpl_args, frame)
                 if value == nil then
               
                 if tpl_args.modifier_ids == nil then
                     return
                     return
                 end
                 end
                tpl_args.mods = m_cargo.array_query{
                    tables={'mods'},
                    fields={'mods.stat_text'},
                    id_field='mods.id',
                    id_array=value
                }
                  
                  
                 local query_ids = {}
                 return value
                for i, mod_id in ipairs(tpl_args.modifier_ids) do
            end,
                    query_ids[#query_ids+1] = string.format('mods.id="%s"', mod_id)
            default = {},
        },
        stat_text = {
            field = 'stat_text',
            type = 'Text',
            func = function (tpl_args, value)
                if tpl_args.mods == nil then
                    return
                 end
                 end
               
                local results = cargo.query(
                    'mods',
                    'mods.id, mods.stat_text',
                    {
                        where=table.concat(query_ids, ' OR '),
                    }
                )
               
                 local text = {}
                 local text = {}
                 for page, row in pairs(results) do
                 for page, row in pairs(tpl_args.mods) do
                     if row['mods.stat_text'] ~= '' then
                     if row['mods.stat_text'] ~= '' then
                         text[#text+1] = row['mods.stat_text']
                         text[#text+1] = row['mods.stat_text']
                     end
                     end
                 end
                 end
                  
                 return table.concat(text, '<br>')
                tpl_args.stat_text = table.concat(text, '<br>')
             end,
             end,
            default = {},
         },
         },
         monster_ids = {
         monster_ids = {
             field = 'monster_ids',
             field = 'monster_ids',
             type = 'List (,) of String',
             type = 'List (,) of String',
            func = factory.arg_list(i18n.args.monster_ids, {key_out='monster_ids'}),
             default = {},
             default = {},
         },
         },
Line 297: Line 274:
             field = 'boss_monster_ids',
             field = 'boss_monster_ids',
             type = 'List (,) of String',
             type = 'List (,) of String',
             func = factory.arg_list(i18n.args.boss_monster_ids, {key_out='boss_monster_ids'}),
             func = function(tpl_args, value)
                if value == nil then
                    return
                end
               
                -- Format the id so it follows cargo standards:
                local id = {}
                for i, v in ipairs(value) do
                    id[#id+1] = string.format('"%s"', v)
                end
               
                -- Query monster data:
                tpl_args._boss_monster_ids = m_cargo.query(
                    {'monsters', 'main_pages'},
                    {
                        'monsters._pageName',
                        'monsters.name',
                        'monsters.metadata_id',
                        'main_pages._pageName',
                    },
                    {
                        join='monsters.metadata_id=main_pages.id',
                        where=string.format(
                            'monsters.metadata_id IN (%s)',
                            table.concat(id, ', ')
                        ),
                    }
                )
               
                return value
            end,
             default = {},
             default = {},
         },
         },
Line 314: Line 321:
         screenshot_ext = {
         screenshot_ext = {
             func = nil,
             func = nil,
            type = 'String',
             default = 'jpg',
             default = 'jpg',
         },
         },
Line 319: Line 327:
             field = 'screenshot',
             field = 'screenshot',
             type = 'Page',
             type = 'Page',
             func = function(tpl_args, frame)
             func = function(tpl_args, value)
                 if tpl_args.name ~= nil then
                 if tpl_args.name ~= nil then
                     tpl_args.screenshot = string.format(i18n.images.screenshot, tpl_args.name, tpl_args.screenshot_ext)
                    -- value contains screenshot id
                     tpl_args.screenshot_infobox = string.format(i18n.images.screenshot_infobox, tpl_args.name, tpl_args.screenshot_ext)
                    local name = value or tpl_args.main_page or tpl_args.name
                     tpl_args.screenshot = string.format(i18n.images.screenshot, name, tpl_args.screenshot_ext)
                     tpl_args.screenshot_infobox = string.format(i18n.images.screenshot_infobox, name, tpl_args.screenshot_ext)
                 end
                 end
             end,
             end,
Line 332: Line 342:
             field = 'vaal_area_ids',
             field = 'vaal_area_ids',
             type = 'List (,) of String',
             type = 'List (,) of String',
            func = factory.arg_list(i18n.args.vaal_area_ids, {key_out='vaal_area_ids'}),
             default = {},
             default = {},
         },
         },
Line 338: Line 347:
             field = 'vaal_area_spawn_chance',
             field = 'vaal_area_spawn_chance',
             type = 'Integer',
             type = 'Integer',
            func = m_util.cast.factory.number(i18n.args.vaal_area_spawn_chance, {key_out='vaal_area_spawn_chance'}),
             default = 0,
             default = 0,
         },
         },
Line 344: Line 352:
             field = 'strongbox_spawn_chance',
             field = 'strongbox_spawn_chance',
             type = 'Integer',
             type = 'Integer',
            func = m_util.cast.factory.number(i18n.args.strongbox_spawn_chance, {key_out='strongbox_spawn_chance'}),
             default = 0,
             default = 0,
         },
         },
Line 350: Line 357:
             field = 'strongbox_max_count',
             field = 'strongbox_max_count',
             type = 'Integer',
             type = 'Integer',
            func = m_util.cast.factory.number(i18n.args.strongbox_max_count, {key_out='strongbox_max_count'}),
             default = 0,
             default = 0,
         },
         },
         -- fields are handled for this below
         -- fields are handled for this below
         strongbox_rarity_weight = {
         strongbox_rarity_weight = {
             func = function (tpl_args, frame)
             func = function (tpl_args, value)
                 local weights = m_util.string.split(tpl_args[i18n.args.strongbox_rarity_weight] or '', ', ')
                 local weights = m_util.string.split(tpl_args['strongbox_rarity_weight'] or '', ', ')
                  
                  
                tpl_args.strongbox_rarity_weight = {}
                 for index, rarity in ipairs(m_game.constants.rarity_order) do
               
                 for index, data in ipairs(m_game.constants.item.rarity) do
                     local value = tonumber(weights[index]) or 0
                     local value = tonumber(weights[index]) or 0
                     tpl_args.strongbox_rarity_weight[data.long_lower] = value
                     -- will be read later
                     tpl_args._properties[string.format('strongbox_weight_%s', data.long_lower)] = value  
                     tpl_args[string.format('strongbox_weight_%s', rarity)] = value
                 end
                 end
             end,
             end,
        },
        strongbox_weight_normal = {
            field = 'strongbox_weight_normal',
            type = 'Integer',
        },
        strongbox_weight_magic = {
            field = 'strongbox_weight_magic',
            type = 'Integer',
        },
        strongbox_weight_rare = {
            field = 'strongbox_weight_rare',
            type = 'Integer',
        },
        strongbox_weight_unique = {
            field = 'strongbox_weight_unique',
            type = 'Integer',
         },
         },
         --
         --
Line 373: Line 393:
             field = 'is_map_area',
             field = 'is_map_area',
             type = 'Boolean',
             type = 'Boolean',
            func = m_util.cast.factory.boolean(i18n.args.is_map_area, {key_out='is_map_area'}),
             default = false,
             default = false,
         },
         },
Line 379: Line 398:
             field = 'is_unique_map_area',
             field = 'is_unique_map_area',
             type = 'Boolean',
             type = 'Boolean',
             func = m_util.cast.factory.boolean(i18n.args.is_unique_map_area, {key_out='is_unique_map_area'}),
            default = false,
        },
        is_legacy_map_area = {
            field = 'is_legacy_map_area',
            type = 'Boolean',
             func = function(tpl_args, value)
                if tpl_args.is_map_area or tpl_args.is_unique_map_area then
                    for _, pattern in ipairs(cfg.legacy_map_area_id_patterns) do
                        if string.find(tpl_args.id, pattern) then
                            return true
                        end
                    end
                end
                return false
            end,
             default = false,
             default = false,
         },
         },
Line 385: Line 418:
             field = 'is_town_area',
             field = 'is_town_area',
             type = 'Boolean',
             type = 'Boolean',
            func = m_util.cast.factory.boolean(i18n.args.is_town_area, {key_out='is_town_area'}),
             default = false,
             default = false,
         },
         },
Line 391: Line 423:
             field = 'is_hideout_area',
             field = 'is_hideout_area',
             type = 'Boolean',
             type = 'Boolean',
            func = m_util.cast.factory.boolean(i18n.args.is_hideout_area, {key_out='is_hideout_area'}),
             default = false,
             default = false,
         },
         },
Line 397: Line 428:
             field = 'is_vaal_area',
             field = 'is_vaal_area',
             type = 'Boolean',
             type = 'Boolean',
            func = m_util.cast.factory.boolean(i18n.args.is_vaal_area, {key_out='is_vaal_area'}),
            default = false,
        },
        is_master_daily_area = {
            field = 'is_is_master_daily_area',
            type = 'Boolean',
            func = m_util.cast.factory.boolean(i18n.args.is_master_daily_area, {key_out='is_master_daily_area'}),
             default = false,
             default = false,
         },
         },
Line 409: Line 433:
             field = 'is_labyrinth_area',
             field = 'is_labyrinth_area',
             type = 'Boolean',
             type = 'Boolean',
            func = m_util.cast.factory.boolean(i18n.args.is_labyrinth_area, {key_out='is_labyrinth_area'}),
             default = false,
             default = false,
         },
         },
Line 415: Line 438:
             field = 'is_labyrinth_airlock_area',
             field = 'is_labyrinth_airlock_area',
             type = 'Boolean',
             type = 'Boolean',
            func = m_util.cast.factory.boolean(i18n.args.is_labyrinth_airlock_area, {key_out='is_labyrinth_airlock_area'}),
             default = false,
             default = false,
         },
         },
Line 421: Line 443:
             field = 'is_labyrinth_boss_area',
             field = 'is_labyrinth_boss_area',
             type = 'Boolean',
             type = 'Boolean',
            func = m_util.cast.factory.boolean(i18n.args.is_labyrinth_boss_area, {key_out='is_labyrinth_boss_area'}),
             default = false,
             default = false,
         },
         },
Line 427: Line 448:
             field = 'has_waypoint',
             field = 'has_waypoint',
             type = 'Boolean',
             type = 'Boolean',
            func = m_util.cast.factory.boolean(i18n.args.has_waypoint, {key_out='has_waypoint'}),
             default = false,
             default = false,
        },
        mainpage_categories = {
            field = 'mainpage_categories',
            type = 'List (,) of String',
            func = function (tpl_args, value)
                -- Category handling for main page only by adding the categories to a cargo field:
                -- Do notice the plural form.
                -- -- Areas, Act X areas/Map areas, Unique map areas
                local cats = {
                    'Category:Areas',
                }
                local cats_ini_len = #cats
                for _, key in ipairs(display.area_type) do
                    if tpl_args[key] == true then
                        cats[#cats+1] = string.format('Category:%ss', i18n.tooltips[key])
                    end
                end
                if #cats == cats_ini_len then
                    cats[#cats+1] = string.format('Category:Act %s areas', tpl_args.act)
                end
                return cats
            end
         },
         },
         --
         --
         -- Handled elsewhere in the old
         -- Handled elsewhere
         --
         --
         release_version = {
         release_version = {
             field = 'release_version',
             field = 'release_version',
             type = 'String'
             type = 'String',
            skip = true,
         },
         },
         removal_version = {
         removal_version = {
             field = 'removal_version',
             field = 'removal_version',
             type = 'String',
             type = 'String',
        },
             skip = true,
        mainpage_categories = {
            field = 'mainpage_categories',
             type = 'List (,) of String',
         },
         },
         infobox_html = {
         infobox_html = {
             field = 'infobox_html',
             field = 'infobox_html',
             type = 'Text',
             type = 'Text',
        },
             skip = true,
        strongbox_weight_normal = {
            field = 'strongbox_weight_normal',
            type = 'Integer',
        },
        strongbox_weight_magic = {
            field = 'strongbox_weight_magic',
            type = 'Integer',
        },
        strongbox_weight_rare = {
            field = 'strongbox_weight_rare',
            type = 'Integer',
        },
        strongbox_weight_unique = {
            field = 'strongbox_weight_unique',
            type = 'Integer',
        },
        stat_text = {
            field = 'stat_text',
             type = 'Text',
         },
         },
         --
         --
Line 473: Line 494:
         --
         --
         areas = {
         areas = {
             func = function(tpl_args, frame)
             func = function(tpl_args, value)
                 local query_ids = {}
                 local query_ids = {}
                 for _, arg in ipairs({'parent_area_id', 'connection_ids', 'vaal_area_ids'}) do  
                 for _, arg in ipairs({'parent_area_id', 'connection_ids', 'vaal_area_ids'}) do  
                     if type(tpl_args[arg]) == 'table' then
                     if type(tpl_args[arg]) == 'table' then
                         for _, tbl_id in ipairs(tpl_args[arg]) do
                         for _, tbl_id in ipairs(tpl_args[arg]) do
                             query_ids[tbl_id] = {}
                             query_ids[tbl_id] = true
                         end
                         end
                     elseif tpl_args[arg] then
                     elseif tpl_args[arg] then
                         query_ids[tpl_args[arg]] = {}
                         query_ids[tpl_args[arg]] = true
                     end
                     end
                 end
                 end
Line 487: Line 508:
                 local query_ids_trimmed = {}
                 local query_ids_trimmed = {}
                 for k, _ in pairs(query_ids) do
                 for k, _ in pairs(query_ids) do
                     if type(k) ~= 'number' then
                     query_ids_trimmed[#query_ids_trimmed+1] = k
                        query_ids_trimmed[#query_ids_trimmed+1] = string.format('areas.id="%s"', k)
                end
                     end
               
                local results=m_cargo.array_query{
                    tables={'areas'},
                    fields={'areas._pageName', 'areas.name', 'areas.main_page'},
                    id_field='areas.id',
                    id_array=query_ids_trimmed,
                    warning_on_missing=true,
                }
                local areas = {}
                for _, data in ipairs(results) do
                     areas[data['areas.id']] = data
                 end
                 end
                  
                  
                 if #query_ids_trimmed == 0 then
                 if tpl_args.is_vaal_area then
                     tpl_args.areas = {}
                     local spawn_areas = {}
                else
                     results = m_cargo.query(
                     local result = cargo.query(
                         {'areas'},
                         'areas',
                         {'areas._pageName', 'areas.id', 'areas.name', 'areas.main_page'},
                         'areas._pageName, areas.id, areas.name, areas.main_page',
                         {
                         {
                             where=table.concat(query_ids_trimmed, ' OR '),
                            -- TODO using a workaround since HOLDS is bricked
                            -- Don't show connected areas without a main_page to avoid showing disabled or areas which are not relevant
                             where=string.format([[
areas.vaal_area_ids__full LIKE "%%%s%%"
AND areas.main_page IS NOT NULL
AND areas.vaal_area_spawn_chance > 0
]], tpl_args.id),
                            -- area id for story areas basically orders them by appearance, should be good enough
                            orderBy='areas.id ASC',
                         }
                         }
                     )
                     )
                      
                      
                    tpl_args.areas = {}
                     for _, data in ipairs(results) do
                     for _, row in ipairs(result) do
                         table.insert(spawn_areas, data['areas.id'])
                         tpl_args.areas[row['areas.id']] = row
                        areas[data['areas.id']] = data
                     end
                     end
                    tpl_args.vaal_spawn_areas = spawn_areas
                 end
                 end
                 -- TODO: Error/Warning for missing areas?
                  
                return areas
             end,
             end,
         },
         },
Line 514: Line 554:
}
}


local argument_order = {
    'main_page',
    'id',
    'name',
    'act',
    'area_level',
    'level_restriction_max',
    'area_type_tags',
    'tags',
    'loading_screen',
    'connection_ids',
    'parent_area_id',
    'modifier_ids',
    'boss_monster_ids',
    'monster_ids',
    'entry_text',
    'entry_npc',
    'flavour_text',
    'screenshot_ext',
    'screenshot',
    'vaal_area_spawn_chance',
    'vaal_area_ids',
    'strongbox_spawn_chance',
    'strongbox_max_count',
    'strongbox_rarity_weight',
    'is_map_area',
    'is_unique_map_area',
    'is_town_area',
    'is_hideout_area',
    'is_vaal_area',
    'is_master_daily_area',
    'is_labyrinth_area',
    'is_labyrinth_airlock_area',
    'is_labyrinth_boss_area',
    'has_waypoint',
    -- parsed by m_util.args.version:
    -- 'release_version',
    -- 'removal_version',
   
    -- Non argument, but passed to the script
    'areas',
}
local display  = {}
display.area_type = {
display.area_type = {
     'is_map_area',
     'is_map_area',
Line 564: Line 560:
     'is_hideout_area',
     'is_hideout_area',
     'is_vaal_area',
     'is_vaal_area',
    'is_master_daily_area',
     'is_labyrinth_area',
     'is_labyrinth_area',
     'is_labyrinth_airlock_area',
     'is_labyrinth_airlock_area',
Line 577: Line 572:
         },
         },
         header = i18n.headers.id,
         header = i18n.headers.id,
         func = function(tpl_args, frame)
         func = function(tpl_args)
             return string.format('[[%s|%s]]', mw.title.getCurrentTitle().fullText, tpl_args.id)
             return string.format('[[%s|%s]]', mw.title.getCurrentTitle().fullText, tpl_args.id)
         end,
         end,
Line 605: Line 600:
         header = i18n.headers.level_restriction_max,
         header = i18n.headers.level_restriction_max,
         func = factory.display_value('level_restriction_max'),
         func = factory.display_value('level_restriction_max'),
    },
    {
        args = {
            boss_monster_ids = {
            },
        },
        header = i18n.headers.boss_monster_ids,
        func = function(tpl_args)
            local out = {}
            for _,v in ipairs(tpl_args._boss_monster_ids) do
                local page = v['main_pages._pageName'] or v['monsters._pageName']
                local name = v['monsters.name'] or v['monsters.metadata_id']
                out[#out+1] = string.format('[[%s|%s]]', page, name)
            end
            return table.concat(out, '<br>')
        end,
     },
     },
     {
     {
Line 612: Line 623:
         },
         },
         header = i18n.headers.area_type_tags,
         header = i18n.headers.area_type_tags,
         func = function(tpl_args, frame)
         func = function(tpl_args)
             return table.concat(tpl_args.area_type_tags, ', ')
             return table.concat(tpl_args.area_type_tags, ', ')
         end,
         end,
Line 622: Line 633:
         },
         },
         header = i18n.headers.tags,
         header = i18n.headers.tags,
         func = function(tpl_args, frame)
         func = function(tpl_args)
             return table.concat(tpl_args.tags, ', ')
             return table.concat(tpl_args.tags, ', ')
         end,
         end,
Line 632: Line 643:
         },
         },
         header = i18n.headers.entry_messsage,
         header = i18n.headers.entry_messsage,
         func = function(tpl_args, frame)
         func = function(tpl_args)
             return m_util.html.poe_color('quest',  -- Any other alternatives for spoken text?
             return m_util.html.poe_color('quest',  -- Any other alternatives for spoken text?
                 string.format(i18n.tooltips.entry_message, tpl_args.entry_npc, tpl_args.entry_text)  
                 string.format(i18n.tooltips.entry_message, tpl_args.entry_npc, tpl_args.entry_text)  
Line 643: Line 654:
         },
         },
         header = i18n.headers.parent_area,
         header = i18n.headers.parent_area,
         func = function(tpl_args, frame)
         func = function(tpl_args)
             return util.display.single_area(tpl_args, frame, tpl_args.parent_area)
             return util.display.single_area(tpl_args, tpl_args.parent_area)
         end,
         end,
     },
     },
Line 652: Line 663:
         },
         },
         header = i18n.headers.connections,
         header = i18n.headers.connections,
         func = function(tpl_args, frame)
         func = function(tpl_args)
             return util.display.multiple_areas(tpl_args, frame, tpl_args.connection_ids)
             return util.display.multiple_areas(tpl_args, tpl_args.connection_ids)
         end,
         end,
     },
     },
Line 661: Line 672:
         },
         },
         header = i18n.headers.vaal_areas,
         header = i18n.headers.vaal_areas,
         func = function(tpl_args, frame)
         func = function(tpl_args)
             return util.display.multiple_areas(tpl_args, frame, tpl_args.vaal_area_ids)
            return util.display.multiple_areas(tpl_args, tpl_args.vaal_area_ids)
        end,
    },
    -- virtual field created by querying other area data to find where a vaal area is used
    {
        args = {
            is_vaal_area = {},
            vaal_spawn_areas = {},
        },
        header = i18n.headers.vaal_spawn_areas,
        func = function(tpl_args)
             return util.display.multiple_areas(tpl_args, tpl_args.vaal_spawn_areas)
         end,
         end,
     },
     },
Line 672: Line 694:
             flavour_text = {},
             flavour_text = {},
         },
         },
         func = function(tpl_args, frame)
         func = function(tpl_args)
             return m_util.html.poe_color('flavour', tpl_args.flavour_text)
             return m_util.html.poe_color('flavour', tpl_args.flavour_text)
         end,
         end,
Line 692: Line 714:
             stat_text = {},
             stat_text = {},
         },
         },
         func = function(tpl_args, frame)
         func = function(tpl_args)
             return m_util.html.poe_color('mod', tpl_args.stat_text)
             return m_util.html.poe_color('mod', tpl_args.stat_text)
         end,
         end,
Line 704: Line 726:
local d = {}
local d = {}


function d.intro_text(tpl_args, frame)
function d.intro_text(tpl_args)
    --[[
    Display an introductory text about the area.
    ]]
     local out = {}
     local out = {}
     if mw.ustring.find(tpl_args['id'], '_') then
     if mw.ustring.find(tpl_args['id'], '_') then
         out[#out+1] = frame:expandTemplate{ title = 'Incorrect title', args = { title=tpl_args['id'] } }
         out[#out+1] = mw.getCurrentFrame():expandTemplate{
            title='Incorrect title',  
            args = {title=tpl_args['id']}  
        }
     end
     end
      
      
     if tpl_args['name'] then
     if tpl_args['name'] then
         out[#out+1] = string.format("'''%s''' is the internal id for the '''%s''' area. ", tpl_args['id'], tpl_args['name'])
         out[#out+1] = string.format(
            i18n.intro.text_with_name,
            tpl_args['id'],  
            tpl_args['main_page'] or tostring(mw.title.getCurrentTitle()),  
            tpl_args['name']
        )
     else  
     else  
         out[#out+1] = string.format("'''%s''' is the internal id of an unnamed area. ", tpl_args['id'])
         out[#out+1] = string.format(
            i18n.intro.text_without_name,
            tpl_args['id']
        )
     end  
     end  


Line 721: Line 757:
             for _, id in ipairs(tpl_args[arg]) do
             for _, id in ipairs(tpl_args[arg]) do
                 if tpl_args.areas[id] then  
                 if tpl_args.areas[id] then  
                     connected_areas[#connected_areas+1] = string.format('<li>[[%s|%s]] (%s)</li>', tostring(tpl_args.areas[id]['areas._pageName']), id, tostring(tpl_args.areas[id]['areas.name']))
                     connected_areas[#connected_areas+1] = string.format(
                        '<li>[[%s|%s]] (%s)</li>',  
                        tostring(tpl_args.areas[id]['areas._pageName']),  
                        id,  
                        tostring(tpl_args.areas[id]['areas.name'])
                    )
                 end
                 end
             end
             end
Line 727: Line 768:
     end
     end
     if #connected_areas > 0 then  
     if #connected_areas > 0 then  
         out[#out+1] = string.format('It is connected to the following areas:<ul>%s</ul>', table.concat(connected_areas))
         out[#out+1] = string.format(
            i18n.intro.connections .. '<ul>%s</ul>',  
            table.concat(connected_areas)
        )
     end
     end
      
      
Line 733: Line 777:
end
end


function d._check_args(tpl_args, frame, data)
function d._check_args(tpl_args, data)
     local continue = true
     local continue = true
     if data.args ~= nil then
     if data.args ~= nil then
Line 761: Line 805:
end
end


function d.area_box(tpl_args, frame)
function d.area_box(tpl_args)
    --[[
    Display the area info box.
    ]]
   
     local infocard_args = {}
     local infocard_args = {}
      
      
Line 792: Line 840:
     local tbl = mw.html.create('table')
     local tbl = mw.html.create('table')
     for _, data in ipairs(display.table_map) do
     for _, data in ipairs(display.table_map) do
         if d._check_args(tpl_args, frame, data) then
         if d._check_args(tpl_args, data) then
             tbl
             tbl
                 :tag('tr')
                 :tag('tr')
Line 799: Line 847:
                         :done()
                         :done()
                     :tag('td')
                     :tag('td')
                         :wikitext(data.func(tpl_args, frame) or '')
                         :wikitext(data.func(tpl_args) or '')
                         :done()
                         :done()
                     :done()
                     :done()
Line 809: Line 857:
     local i = 2
     local i = 2
     for _, data in ipairs(display.list_map) do
     for _, data in ipairs(display.list_map) do
         if d._check_args(tpl_args, frame, data) then
         if d._check_args(tpl_args, data) then
             infocard_args[i] = data.func(tpl_args, frame)
             infocard_args[i] = data.func(tpl_args)
             i = i + 1
             i = i + 1
         end
         end
Line 818: Line 866:
end
end


function d.subobject_box(tpl_args, frame)
function d.subobject_box(tpl_args)
    --[[
    Display the subobject box.
    ]]
     local container = mw.html.create('div')
     local container = mw.html.create('div')
     container
     container
Line 824: Line 875:
          
          
     -- spawn weight table  
     -- spawn weight table  
     tbl = container:tag('table')
     local tbl = container:tag('table')
     tbl
     tbl
         :attr('class', 'wikitable sortable')
         :attr('class', 'wikitable sortable')
Line 877: Line 928:


-- ----------------------------------------------------------------------------
-- ----------------------------------------------------------------------------
-- Page functions
-- Main functions
-- ----------------------------------------------------------------------------
-- ----------------------------------------------------------------------------


local p = {}
local function _area(tpl_args)
 
     --[[
p.table_areas = m_util.cargo.declare_factory{data=argument_map}
    This function adds cargo tables and displays information about the
 
     area.
function p.area(frame)
     -- = p.area{id = '1_1_1', name = 'The Twilight Strand', act = 1, level = 1, tags = 'no_tempests, area_with_water', loading_screen = 'Act1', connection_ids = '1_1_town',parent_area_id = '1_1_town',  boss_monster_ids = 'Metadata/Monsters/ZombieBoss/ZombieBossHillockNormal', flavour_text = 'Hope was drowned here.', main_page = 'The Twilight Strand (Act 1)'}
     -- = p.area{id = '1_4_2', name = 'The Dried Lake', act = '4', level = '34', area_type_tags = 'shore', tags = 'act_boss_area', loading_screen = 'Act4', connection_ids = '1_4_town', parent_area_id = '1_4_town', boss_monster_ids = 'Metadata/Monsters/Voll/VollBoss, Metadata/Monsters/SkeletonSoldier/SkeletonSoldierRangedBoss', vaal_area_spawn_chance = '18', vaal_area_ids = '1_SideArea4_2, 1_SideArea4_4', strongbox_spawn_chance = '30', strongbox_max = '2', strongbox_rarity_weight = '50, 50, 50, 1', flavour_text = 'Bones of betrayal, ashes of purity.', main_page = 'The Dried Lake'}
      
      
     local tpl_args = getArgs(frame, {
     Examples
         parentFirst = true
    --------
     })
    = p.area{
     frame = m_util.misc.get_frame(frame)
        id = '1_1_1',
        name = 'The Twilight Strand',
        act = 1,
        level = 1,
        tags = 'no_tempests, area_with_water',
        loading_screen = 'Act1',
        connection_ids = '1_1_town',
        parent_area_id = '1_1_town', 
        boss_monster_ids = 'Metadata/Monsters/ZombieBoss/ZombieBossHillockNormal',
        flavour_text = 'Hope was drowned here.',  
         main_page = 'The Twilight Strand (Act 1)'
     }
     = p.area{
        id = '1_1_1',
        name = 'The Twilight Strand',
        act = 1,
        level = 1,
        tags = 'no_tempests, area_with_water',
        loading_screen = 'Act1',
        connection_ids = '1_1_town',
        parent_area_id = '1_1_town', 
        boss_monster_ids = 'Metadata/Monsters/ZombieBoss/ZombieBossHillockNormal',
        flavour_text = 'Hope was drowned here.', main_page = 'The Twilight Strand (Act 1)'
    }
    = p.area{
        id = '1_4_2',
        name = 'The Dried Lake',
        act = '4',
        level = '34',
        area_type_tags = 'shore',
        tags = 'act_boss_area',
        loading_screen = 'Act4',
        connection_ids = '1_4_town',
        parent_area_id = '1_4_town',
        boss_monster_ids = 'Metadata/Monsters/Voll/VollBoss, Metadata/Monsters/SkeletonSoldier/SkeletonSoldierRangedBoss',
        vaal_area_spawn_chance = '18',
        vaal_area_ids = '1_SideArea4_2, 1_SideArea4_4',
        strongbox_spawn_chance = '30',
        strongbox_max = '2', strongbox_rarity_weight = '50, 50, 50, 1',
        flavour_text = 'Bones of betrayal, ashes of purity.', main_page = 'The Dried Lake'
    }
    ]]
      
      
     --
     --
     -- Shared args
     -- Shared args
     --
     --
    tpl_args._properties = {
        _table = argument_map.table,
    }
   
     -- Handle release_version and removal_version
     -- Handle release_version and removal_version
     m_util.args.version(tpl_args, {frame=frame})
     m_util.args.version(tpl_args)
      
      
     -- Parse args
     local cargo_values = m_util.args.from_cargo_map{
    for _, k in ipairs(argument_order) do
         tpl_args=tpl_args,
        local data = argument_map.fields[k]
         table_map=argument_map,
        if data == nil then
         rtr=true,
            error('Missing data in argument_map: ' .. k)
     }
         end
        if data.func ~= nil then
            data.func(tpl_args, frame)
        end
          
        if data.default ~= nil and tpl_args[k] == nil then
            tpl_args[k] = data.default
        end
    end
   
    -- this should include all values as long they're set, regardless of whether they are parsed above
    for key, data in pairs(argument_map.fields) do
         local v = tpl_args[key]
        if data.field ~= nil and v ~= nil then
            tpl_args._properties[data.field] = v
        end
     end
      
      
     -- Parse spawn weights
     -- Parse spawn weights
     m_util.args.weight_list(tpl_args, {
     m_util.args.spawn_weight_list(tpl_args)
        frame=frame,
        output_argument='spawn_weights',
    })
   
    -- Category handling for main page only by adding the categories in a property:
    -- Do notice the plural form.
    -- -- Areas, Act X areas/Map areas, Unique map areas
    local cats = {
        'Category:Areas',
    }
    local cats_ini_len = #cats
    for _, key in ipairs(display.area_type) do
        if tpl_args[key] == true then
            cats[#cats+1] = string.format('Category:%ss', i18n.tooltips[key])
        end
    end
    if #cats == cats_ini_len then
        cats[#cats+1] = string.format('Category:Act %s areas', tpl_args.act)
    end
 
    tpl_args._properties['mainpage_categories'] = table.concat(cats, ',')
   
    -- Category handling for the local data page:
    local page_cats = {
        'Area data',
    }
      
      
     -- Display only on main pages:
     -- Display only on main pages:
     local out = {}
     local out = {}
     out[#out+1] = d.area_box(tpl_args, frame)
     out[#out+1] = d.area_box(tpl_args)
      
      
     -- Property to store what's output to main pages:
     -- Property to store what's output to main pages:
     tpl_args._properties['infobox_html'] = out[1]
     cargo_values['infobox_html'] = out[1]
      
      
     -- Set all semantic properties:
     -- Set all semantic properties:
     mw.logObject('Cargo:' .. m_util.cargo.store(frame, tpl_args._properties))
     mw.logObject('Cargo:' .. m_cargo.store(cargo_values))
     mw.logObject(tpl_args._properties)
 
   
     -- mw.logObject(tpl_args)
     -- Display only on data page:
     -- Display only on data page:
     out[#out+1] = d.subobject_box(tpl_args, frame)
     out[#out+1] = d.subobject_box(tpl_args)
     out[#out+1] = d.intro_text(tpl_args, frame)
     out[#out+1] = d.intro_text(tpl_args)
 
    -- Attach to table
    mw.getCurrentFrame():expandTemplate{title = i18n.templates.attach_template}
 
    -- Category handling for the local data page:
    out[#out+1] = m_util.misc.add_category({i18n.categories.area_data})
      
      
     -- Output of function
     -- Output of function
     return table.concat(out) .. m_util.misc.add_category(page_cats)
     return table.concat(out)
end
end


function p.query_area_info(frame)
local function _query_area_info(tpl_args)
     -- p.query_area_info{conditions = '[[Is area id::test]]', cats='yes'}
     --[[
    local tpl_args = getArgs(frame, {
    Queries and displays the area infobox.
        parentFirst = true
   
    })
    ]]
    frame = m_util.misc.get_frame(frame)
    -- = p.query_area_info{conditions = '[[Is area id::test]]', cats='yes'}
      
      
     if tpl_args.where == nil then
     if tpl_args.where == nil then
Line 988: Line 1,035:
     tpl_args.cats = m_util.cast.boolean(tpl_args.cats)
     tpl_args.cats = m_util.cast.boolean(tpl_args.cats)
      
      
     local results = cargo.query(
     local results = m_cargo.query(
         'areas',
         {'areas'},
         'areas.infobox_html, areas.mainpage_categories',
         {'areas.infobox_html', 'areas.mainpage_categories'},
         {
         {
             where=tpl_args.where,
             where=tpl_args.where,
             orderBy=tpl_args.order_by,
             orderBy=tpl_args.order_by,
            -- Temporary fix for cargo duplicating entries
            groupBy='areas._pageID',
         }
         }
     )
     )
Line 1,016: Line 1,061:
     end
     end
end
end
-- ----------------------------------------------------------------------------
-- Exported functions
-- ----------------------------------------------------------------------------
local p = {}
p.table_areas = m_cargo.declare_factory{data=argument_map}
--
-- Template:Area
--
p.area = m_util.misc.invoker_factory(_area, {
    wrappers = cfg.wrappers.area,
})
--
-- Template:Query area infoboxes
--
p.query_area_info = m_util.misc.invoker_factory(_query_area_info, {
    wrappers = cfg.wrappers.query_area_info,
})


return p
return p

Latest revision as of 06:43, 9 October 2024

Module documentation[create] [purge]
-------------------------------------------------------------------------------
-- 
--                             Module:Area
-- 
-- This module implements Template:Area and Template:Query area infoboxes
-------------------------------------------------------------------------------

-- ----------------------------------------------------------------------------
-- TODO
-- ----------------------------------------------------------------------------
-- invalid tags -> utl?
-- spawn_weight* values
-- spawnchacne values

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

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

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

local f_infocard = require('Module:Infocard')._main

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

local i18n = cfg.i18n

-- ----------------------------------------------------------------------------
-- Utility & helper functions
-- ----------------------------------------------------------------------------

local h = {}

--
-- Functions for processing tpl_args
--
h.proc = {}
h.proc.factory = {}

function h.proc.factory.list(args)
    args = args or {}
    return function (tpl_args, value)
        return m_util.cast.table(value, {
            pattern = args.pattern,
            callback = args.callback,
        })
    end
end

local factory = {}

function factory.display_value(k, args)
    return function(tpl_args)
        return tpl_args[k]
    end
end

local util = {}
util.display = {}
function util.display.multiple_areas(tpl_args, area_ids)
    local out = {}
    for _, area_id in ipairs(area_ids) do
        out[#out+1] = util.display.single_area(tpl_args, area_id)
    end
    return table.concat(out, '<br>')
end

function util.display.single_area(tpl_args, area_id)
    if tpl_args.areas[area_id] then
        if tpl_args.areas[area_id]['areas.main_page'] then
            return string.format('[[%s|%s]]', tpl_args.areas[area_id]['areas.main_page'], tpl_args.areas[area_id]['areas.name'])
        else
            return string.format('%s ([[%s|%s]])', tpl_args.areas[area_id]['areas.name'], tpl_args.areas[area_id]['areas._pageName'], area_id)
        end
    else
        return area_id
    end
end

-- ----------------------------------------------------------------------------
-- Argument & display mapping
-- ----------------------------------------------------------------------------
local display  = {}

local argument_map = {
    table = 'areas',
    
    order = {
        'main_page',
        'id',
        'name',
        'act',
        'area_level',
        'level_restriction_max',
        'area_type_tags',
        'tags',
        'loading_screen',
        'connection_ids',
        'parent_area_id',
        'modifier_ids',
        -- populated via modifier ids
        'stat_text',
        'boss_monster_ids',
        'monster_ids',
        'entry_text',
        'entry_npc',
        'flavour_text',
        'screenshot_ext',
        'screenshot',
        'vaal_area_spawn_chance',
        'vaal_area_ids',
        'strongbox_spawn_chance',
        'strongbox_max_count',
        'strongbox_rarity_weight',
        -- those four are parsed via the argument above
        'strongbox_weight_normal',
        'strongbox_weight_magic',
        'strongbox_weight_rare',
        'strongbox_weight_unique',
        'is_map_area',
        'is_unique_map_area',
        'is_legacy_map_area',
        'is_town_area',
        'is_hideout_area',
        'is_vaal_area',
        'is_labyrinth_area',
        'is_labyrinth_airlock_area',
        'is_labyrinth_boss_area',
        'has_waypoint',
        'mainpage_categories',
        
        -- Non argument, but passed to the script
        'areas',
    },
    
    --
    -- User supplied arguments
    -- 
    fields = {
        main_page = {
            field = 'main_page', 
            type = 'Page',
            func = function(tpl_args, value)
                if value ~= nil then
                    local page = mw.title.new(value)
                    if page == nil then
                        error(string.format(i18n.errors.main_page_is_invalid, value))
                    elseif not page.exists then
                        error(string.format(i18n.errors.main_page_does_not_exist, value))
                    else
                        -- dont need the title object anymore
                        --tpl_args.main_page = page
                    end
                end
                return value
            end
        },
        
        --
        -- Can be populated by PyPoE
        --
        id = {
            field = 'id',
            type = 'String',
            func = nil,
        },
        name = {
            field = 'name',
            type = 'String',
            func = nil,
        },
        act = {
            field = 'act',
            type = 'Integer',
        },
        area_level = {
            field = 'area_level',
            type = 'Integer',
        },
        level_restriction_max = {
            field = 'level_restriction_max',
            type = 'Integer',
            default = 100,
        },
        area_type_tags = {
            field = 'area_type_tags',
            type = 'List (,) of String',
            func = h.proc.factory.list{
                callback = m_util.validate.factory.in_table_keys{
                    tbl = m_game.constants.tags,
                    errmsg = i18n.errors.invalid_tag,
                    errlvl = 4,
                },
            },
            default = {},
        },
        tags = {
            field = 'tags',
            type = 'List (,) of String',
            func = h.proc.factory.list{
                callback = m_util.validate.factory.in_table_keys{
                    tbl = m_game.constants.tags,
                    errmsg = i18n.errors.invalid_tag,
                    errlvl = 4,
                },
            },
            default = {},
        },
        loading_screen = {
            field = 'loading_screen',
            type = 'Page',
            func = function (tpl_args, value)
                if value ~= nil then
                    -- value contains loading id
                    tpl_args.loading_screen_infobox = string.format(i18n.images.loading_screen_infobox, value)
                    
                    return string.format(i18n.images.loading_screen, value)
                end
            end,
        },
        connection_ids = {
            field = 'connection_ids',
            type = 'List (,) of String',
            default = {},
        },
        parent_area_id = {
            field = 'parent_area_id',
            type = 'String',
        },
        modifier_ids = {
            field = 'modifier_ids',
            type = 'List (,) of String',
            func = function(tpl_args, value)
                if value == nil then
                    return
                end
                tpl_args.mods = m_cargo.array_query{
                    tables={'mods'},
                    fields={'mods.stat_text'},
                    id_field='mods.id',
                    id_array=value
                }
                
                return value
            end,
            default = {},
        },
        stat_text = {
            field = 'stat_text',
            type = 'Text',
            func = function (tpl_args, value)
                if tpl_args.mods == nil then
                    return
                end
                local text = {}
                for page, row in pairs(tpl_args.mods) do
                    if row['mods.stat_text'] ~= '' then
                        text[#text+1] = row['mods.stat_text']
                    end
                end
                return table.concat(text, '<br>')
            end,
        },
        monster_ids = {
            field = 'monster_ids',
            type = 'List (,) of String',
            default = {},
        },
        boss_monster_ids = {
            field = 'boss_monster_ids',
            type = 'List (,) of String',
            func = function(tpl_args, value)
                if value == nil then
                    return
                end
                
                -- Format the id so it follows cargo standards:
                local id = {}
                for i, v in ipairs(value) do 
                    id[#id+1] = string.format('"%s"', v)
                end 
                
                -- Query monster data:
                tpl_args._boss_monster_ids = m_cargo.query(
                    {'monsters', 'main_pages'},
                    {
                        'monsters._pageName', 
                        'monsters.name', 
                        'monsters.metadata_id',
                        'main_pages._pageName',
                    },
                    {
                        join='monsters.metadata_id=main_pages.id',
                        where=string.format(
                            'monsters.metadata_id IN (%s)', 
                            table.concat(id, ', ')
                        ),
                    }
                )
                
                return value
            end,
            default = {},
        },
        entry_text = {
            field = 'entry_text',
            type = 'Text',
        },
        entry_npc = {
            field = 'entry_npc',
            type = 'String',
        },
        flavour_text = {
            field = 'flavour_text',
            type = 'Text',
        },
        screenshot_ext = {
            func = nil,
            type = 'String',
            default = 'jpg',
        },
        screenshot = {
            field = 'screenshot',
            type = 'Page',
            func = function(tpl_args, value)
                if tpl_args.name ~= nil then
                    -- value contains screenshot id
                    local name = value or tpl_args.main_page or tpl_args.name
                    tpl_args.screenshot = string.format(i18n.images.screenshot, name, tpl_args.screenshot_ext)
                    tpl_args.screenshot_infobox = string.format(i18n.images.screenshot_infobox, name, tpl_args.screenshot_ext)
                end
            end,
        },
        --
        -- Spawn chances
        --
        vaal_area_ids = {
            field = 'vaal_area_ids',
            type = 'List (,) of String',
            default = {},
        },
        vaal_area_spawn_chance = {
            field = 'vaal_area_spawn_chance',
            type = 'Integer',
            default = 0,
        },
        strongbox_spawn_chance = {
            field = 'strongbox_spawn_chance',
            type = 'Integer',
            default = 0,
        },
        strongbox_max_count = {
            field = 'strongbox_max_count',
            type = 'Integer',
            default = 0,
        },
        -- fields are handled for this below
        strongbox_rarity_weight = {
            func = function (tpl_args, value)
                local weights = m_util.string.split(tpl_args['strongbox_rarity_weight'] or '', ', ')
                
                for index, rarity in ipairs(m_game.constants.rarity_order) do
                    local value = tonumber(weights[index]) or 0
                    -- will be read later
                    tpl_args[string.format('strongbox_weight_%s', rarity)] = value
                end
            end,
        },
        strongbox_weight_normal = {
            field = 'strongbox_weight_normal',
            type = 'Integer',
        },
        strongbox_weight_magic = {
            field = 'strongbox_weight_magic',
            type = 'Integer',
        },
        strongbox_weight_rare = {
            field = 'strongbox_weight_rare',
            type = 'Integer',
        },
        strongbox_weight_unique = {
            field = 'strongbox_weight_unique',
            type = 'Integer',
        },
        --
        -- Area flags
        --
        is_map_area = {
            field = 'is_map_area',
            type = 'Boolean',
            default = false,
        },
        is_unique_map_area = {
            field = 'is_unique_map_area',
            type = 'Boolean',
            default = false,
        },
        is_legacy_map_area = {
            field = 'is_legacy_map_area',
            type = 'Boolean',
            func = function(tpl_args, value)
                if tpl_args.is_map_area or tpl_args.is_unique_map_area then
                    for _, pattern in ipairs(cfg.legacy_map_area_id_patterns) do
                        if string.find(tpl_args.id, pattern) then
                            return true
                        end
                    end
                end
                return false
            end,
            default = false,
        },
        is_town_area = {
            field = 'is_town_area',
            type = 'Boolean',
            default = false,
        },
        is_hideout_area = {
            field = 'is_hideout_area',
            type = 'Boolean',
            default = false,
        },
        is_vaal_area = {
            field = 'is_vaal_area',
            type = 'Boolean',
            default = false,
        },
        is_labyrinth_area = {
            field = 'is_labyrinth_area',
            type = 'Boolean',
            default = false,
        },
        is_labyrinth_airlock_area = {
            field = 'is_labyrinth_airlock_area',
            type = 'Boolean',
            default = false,
        },
        is_labyrinth_boss_area = {
            field = 'is_labyrinth_boss_area',
            type = 'Boolean',
            default = false,
        },
        has_waypoint = {
            field = 'has_waypoint',
            type = 'Boolean',
            default = false,
        },
        mainpage_categories = {
            field = 'mainpage_categories',
            type = 'List (,) of String',
            func = function (tpl_args, value)
                -- Category handling for main page only by adding the categories to a cargo field:
                -- Do notice the plural form.
                -- -- Areas, Act X areas/Map areas, Unique map areas 
                local cats = {
                    'Category:Areas', 
                }
                local cats_ini_len = #cats
                for _, key in ipairs(display.area_type) do
                    if tpl_args[key] == true then
                        cats[#cats+1] = string.format('Category:%ss', i18n.tooltips[key])
                    end
                end
                if #cats == cats_ini_len then 
                    cats[#cats+1] = string.format('Category:Act %s areas', tpl_args.act)
                end
                return cats
            end
        },
        --
        -- Handled elsewhere
        --
        release_version = {
            field = 'release_version',
            type = 'String',
            skip = true,
        },
        removal_version = {
            field = 'removal_version',
            type = 'String',
            skip = true,
        },
        infobox_html = {
            field = 'infobox_html',
            type = 'Text',
            skip = true,
        },
        --
        -- Not argument to the template, but still parsing arguments
        --
        areas = {
            func = function(tpl_args, value)
                local query_ids = {}
                for _, arg in ipairs({'parent_area_id', 'connection_ids', 'vaal_area_ids'}) do 
                    if type(tpl_args[arg]) == 'table' then
                        for _, tbl_id in ipairs(tpl_args[arg]) do
                            query_ids[tbl_id] = true
                        end
                    elseif tpl_args[arg] then
                        query_ids[tpl_args[arg]] = true
                    end
                end
                
                local query_ids_trimmed = {}
                for k, _ in pairs(query_ids) do
                    query_ids_trimmed[#query_ids_trimmed+1] = k
                end
                
                local results=m_cargo.array_query{
                    tables={'areas'},
                    fields={'areas._pageName', 'areas.name', 'areas.main_page'},
                    id_field='areas.id',
                    id_array=query_ids_trimmed,
                    warning_on_missing=true,
                }
                local areas = {}
                for _, data in ipairs(results) do
                    areas[data['areas.id']] = data
                end
                
                if tpl_args.is_vaal_area then
                    local spawn_areas = {}
                    results = m_cargo.query(
                        {'areas'},
                        {'areas._pageName', 'areas.id', 'areas.name', 'areas.main_page'},
                        {
                            -- TODO using a workaround since HOLDS is bricked
                            -- Don't show connected areas without a main_page to avoid showing disabled or areas which are not relevant
                            where=string.format([[
areas.vaal_area_ids__full LIKE "%%%s%%"
AND areas.main_page IS NOT NULL
AND areas.vaal_area_spawn_chance > 0
]], tpl_args.id),
                            -- area id for story areas basically orders them by appearance, should be good enough
                            orderBy='areas.id ASC',
                        }
                    )
                    
                    for _, data in ipairs(results) do
                        table.insert(spawn_areas, data['areas.id'])
                        areas[data['areas.id']] = data
                    end
                    tpl_args.vaal_spawn_areas = spawn_areas
                end
                
                return areas
            end,
        },
    },
}

display.area_type = {
    'is_map_area',
    'is_unique_map_area',
    'is_town_area',
    'is_hideout_area',
    'is_vaal_area',
    'is_labyrinth_area',
    'is_labyrinth_airlock_area',
    'is_labyrinth_boss_area',
}

display.table_map = {
    {
        args = {
            id = {
            },
        },
        header = i18n.headers.id,
        func = function(tpl_args)
            return string.format('[[%s|%s]]', mw.title.getCurrentTitle().fullText, tpl_args.id)
        end,
    },
    {
        args = {
            act = {
            },
        },
        header = i18n.headers.act,
        func = factory.display_value('act'),
    }, 
    {
        args = {
            area_level = {
            },
        },
        header = i18n.headers.area_level,
        func = factory.display_value('area_level'),
    },
    {
        args = {
            level_restriction_max = {
                hide = 100,
            },
        },
        header = i18n.headers.level_restriction_max,
        func = factory.display_value('level_restriction_max'),
    },
    {
        args = {
            boss_monster_ids = {
            },
        },
        header = i18n.headers.boss_monster_ids,
        func = function(tpl_args)
            local out = {}
            for _,v in ipairs(tpl_args._boss_monster_ids) do 
                local page = v['main_pages._pageName'] or v['monsters._pageName']
                local name = v['monsters.name'] or v['monsters.metadata_id']
                out[#out+1] = string.format('[[%s|%s]]', page, name)
            end 
            return table.concat(out, '<br>')
        end,
    },
    {
        args = {
            area_type_tags = {
            },
        },
        header = i18n.headers.area_type_tags,
        func = function(tpl_args)
            return table.concat(tpl_args.area_type_tags, ', ')
        end,
    },
    {
        args = {
            tags = {
            },
        },
        header = i18n.headers.tags,
        func = function(tpl_args)
            return table.concat(tpl_args.tags, ', ')
        end,
    },
    {
        args = {
            entry_text = {},
            entry_npc = {},
        },
        header = i18n.headers.entry_messsage,
        func = function(tpl_args)
            return m_util.html.poe_color('quest',  -- Any other alternatives for spoken text?
                string.format(i18n.tooltips.entry_message, tpl_args.entry_npc, tpl_args.entry_text) 
            )
        end,
    },
    {
        args = {
            parent_area_ids = {},
        },
        header = i18n.headers.parent_area,
        func = function(tpl_args)
            return util.display.single_area(tpl_args, tpl_args.parent_area)
        end,
    },
    {
        args = {
            connection_ids = {},
        },
        header = i18n.headers.connections,
        func = function(tpl_args)
            return util.display.multiple_areas(tpl_args, tpl_args.connection_ids)
        end,
    },
    {
        args = {
            vaal_area_ids = {},
        },
        header = i18n.headers.vaal_areas,
        func = function(tpl_args)
            return util.display.multiple_areas(tpl_args, tpl_args.vaal_area_ids)
        end,
    },
    -- virtual field created by querying other area data to find where a vaal area is used
    {
        args = {
            is_vaal_area = {},
            vaal_spawn_areas = {},
        },
        header = i18n.headers.vaal_spawn_areas,
        func = function(tpl_args)
            return util.display.multiple_areas(tpl_args, tpl_args.vaal_spawn_areas)
        end,
    },
}

display.list_map = {
    {
        args = {
            flavour_text = {},
        },
        func = function(tpl_args)
            return m_util.html.poe_color('flavour', tpl_args.flavour_text)
        end,
    },
    {
        args = {
            loading_screen_infobox = {},
        },
        func = factory.display_value('loading_screen_infobox'),
    },
    {
        args = {
            screenshot_infobox = {},
        },
        func = factory.display_value('screenshot_infobox'),
    },
    {
        args = {
            stat_text = {},
        },
        func = function(tpl_args)
            return m_util.html.poe_color('mod', tpl_args.stat_text)
        end,
    },
}

-- ----------------------------------------------------------------------------
-- display functions
-- ----------------------------------------------------------------------------

local d = {}

function d.intro_text(tpl_args)
    --[[
    Display an introductory text about the area.
    ]]
    local out = {}
    if mw.ustring.find(tpl_args['id'], '_') then
        out[#out+1] = mw.getCurrentFrame():expandTemplate{
            title='Incorrect title', 
            args = {title=tpl_args['id']} 
        }
    end
    
    if tpl_args['name'] then
        out[#out+1] = string.format(
            i18n.intro.text_with_name, 
            tpl_args['id'], 
            tpl_args['main_page'] or tostring(mw.title.getCurrentTitle()), 
            tpl_args['name']
        )
    else 
        out[#out+1] = string.format(
            i18n.intro.text_without_name,
            tpl_args['id']
        )
    end 

    local connected_areas = {}
    for _, arg in ipairs({'connection_ids', 'vaal_area_ids'}) do  
        if tpl_args[arg] then
            for _, id in ipairs(tpl_args[arg]) do
                if tpl_args.areas[id] then 
                    connected_areas[#connected_areas+1] = string.format(
                        '<li>[[%s|%s]] (%s)</li>', 
                        tostring(tpl_args.areas[id]['areas._pageName']), 
                        id, 
                        tostring(tpl_args.areas[id]['areas.name'])
                    )
                end
            end
        end
    end
    if #connected_areas > 0 then 
        out[#out+1] = string.format(
            i18n.intro.connections .. '<ul>%s</ul>', 
            table.concat(connected_areas)
        )
    end
    
    return table.concat(out)
end

function d._check_args(tpl_args, data)
    local continue = true
    if data.args ~= nil then
        for key, key_data in pairs(data.args) do
            if tpl_args[key] == nil or (type(tpl_args[key]) == 'table' and #tpl_args[key] == 0) then
                continue = false
                break
            elseif type(key_data.hide) == 'table' then
                local br = false
                for _, value in ipairs(key_data.hide) do
                    if tpl_args[key] == value then
                        br = true
                        break
                    end
                end
                if br then
                    continue = false
                    break
                end
            elseif key_data.hide ~= nil and tpl_args[key] == key_data.hide then
                continue = false
                break
            end
        end
    end
    return continue
end

function d.area_box(tpl_args)
    --[[
    Display the area info box.
    ]]
    
    local infocard_args = {}
    
    infocard_args.header = tpl_args.name
    
    -- Subheader
    local out = {}
    for _, key in ipairs(display.area_type) do
        if tpl_args[key] == true then
            out[#out+1] = i18n.tooltips[key]
        end
    end

    if #out > 0 then
        infocard_args.subheader = table.concat(out, ', ')
    else
        infocard_args.subheader = i18n.tooltips.area
    end
    
    -- Side header
    if tpl_args.is_town_area then
        infocard_args.headerright = i18n.images.waypoint_town
    elseif tpl_args.has_waypoint then
        infocard_args.headerright = i18n.images.waypoint_yes
    else
        infocard_args.headerright = i18n.images.waypoint_no
    end
    
    -- Main sections, loop through
    local tbl = mw.html.create('table')
    for _, data in ipairs(display.table_map) do
        if d._check_args(tpl_args, data) then
            tbl
                :tag('tr')
                    :tag('th')
                        :wikitext(data.header or '')
                        :done()
                    :tag('td')
                        :wikitext(data.func(tpl_args) or '')
                        :done()
                    :done()
        end
    end
    
    infocard_args[1] = tostring(tbl)
    
    local i = 2
    for _, data in ipairs(display.list_map) do
        if d._check_args(tpl_args, data) then
            infocard_args[i] = data.func(tpl_args)
            i = i + 1
        end
    end

    return f_infocard(infocard_args)
end

function d.subobject_box(tpl_args)
    --[[
    Display the subobject box.
    ]]
    local container = mw.html.create('div')
    container
        :attr('class', 'modbox floatright')
        
    -- spawn weight table 
    local tbl = container:tag('table')
    tbl
        :attr('class', 'wikitable sortable')
        -- :attr('style', 'style="width: 100%;"')
        :tag('tr')
            :tag('th')
                :attr('colspan', 3)
                :wikitext('Spawn Weights')
                :done()
            :done()
        :tag('tr')
            :tag('th')
                :wikitext('#')
                :done()
            :tag('th')
                :wikitext('Tag')
                :done()
            :tag('th')
                :wikitext('Has spawn weight')
                :done()
            :done()
        :done()
        
    local i = 0
    local value = nil
    repeat
        i = i + 1
        value = {
            tag = tpl_args[string.format('spawn_weight%s_tag', i)],
            value = tpl_args[string.format('spawn_weight%s_value', i)],
        }
        
        if value.tag then
            tbl
                :tag('tr')
                    :tag('td')
                        :wikitext(i)
                        :done()
                    :tag('td')
                        :wikitext(value.tag)
                        :done()
                    :tag('td')
                        :wikitext(value.value)
                        :done()
                    :done()
                :done()
        end
    until value.tag == nil
    
    return tostring(container)
end

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

local function _area(tpl_args)
    --[[
    This function adds cargo tables and displays information about the 
    area.
    
    Examples
    --------
    = p.area{
        id = '1_1_1', 
        name = 'The Twilight Strand', 
        act = 1, 
        level = 1, 
        tags = 'no_tempests, area_with_water', 
        loading_screen = 'Act1', 
        connection_ids = '1_1_town',
        parent_area_id = '1_1_town',  
        boss_monster_ids = 'Metadata/Monsters/ZombieBoss/ZombieBossHillockNormal', 
        flavour_text = 'Hope was drowned here.', 
        main_page = 'The Twilight Strand (Act 1)'
    }  
    = p.area{
        id = '1_1_1', 
        name = 'The Twilight Strand', 
        act = 1, 
        level = 1, 
        tags = 'no_tempests, area_with_water', 
        loading_screen = 'Act1', 
        connection_ids = '1_1_town',
        parent_area_id = '1_1_town',  
        boss_monster_ids = 'Metadata/Monsters/ZombieBoss/ZombieBossHillockNormal', 
        flavour_text = 'Hope was drowned here.', main_page = 'The Twilight Strand (Act 1)'
    }
    = p.area{
        id = '1_4_2', 
        name = 'The Dried Lake', 
        act = '4', 
        level = '34', 
        area_type_tags = 'shore', 
        tags = 'act_boss_area', 
        loading_screen = 'Act4', 
        connection_ids = '1_4_town', 
        parent_area_id = '1_4_town', 
        boss_monster_ids = 'Metadata/Monsters/Voll/VollBoss, Metadata/Monsters/SkeletonSoldier/SkeletonSoldierRangedBoss', 
        vaal_area_spawn_chance = '18', 
        vaal_area_ids = '1_SideArea4_2, 1_SideArea4_4', 
        strongbox_spawn_chance = '30', 
        strongbox_max = '2', strongbox_rarity_weight = '50, 50, 50, 1', 
        flavour_text = 'Bones of betrayal, ashes of purity.', main_page = 'The Dried Lake'
    }
    ]]
    
    --
    -- Shared args
    --
    -- Handle release_version and removal_version
    m_util.args.version(tpl_args)
    
    local cargo_values = m_util.args.from_cargo_map{
        tpl_args=tpl_args,
        table_map=argument_map,
        rtr=true,
    }
    
    -- Parse spawn weights
    m_util.args.spawn_weight_list(tpl_args)
    
    -- Display only on main pages:
    local out = {}
    out[#out+1] = d.area_box(tpl_args)
    
    -- Property to store what's output to main pages:
    cargo_values['infobox_html'] = out[1]
    
    -- Set all semantic properties:
    mw.logObject('Cargo:' .. m_cargo.store(cargo_values))

    -- mw.logObject(tpl_args)
    -- Display only on data page:
    out[#out+1] = d.subobject_box(tpl_args)
    out[#out+1] = d.intro_text(tpl_args)

    -- Attach to table
    mw.getCurrentFrame():expandTemplate{title = i18n.templates.attach_template}

    -- Category handling for the local data page:
    out[#out+1] = m_util.misc.add_category({i18n.categories.area_data})
    
    -- Output of function
    return table.concat(out)
end

local function _query_area_info(tpl_args)
    --[[
    Queries and displays the area infobox. 
    
    ]]
    -- = p.query_area_info{conditions = '[[Is area id::test]]', cats='yes'}
    
    if tpl_args.where == nil then
        return
    end
    
    tpl_args.cats = m_util.cast.boolean(tpl_args.cats)
    
    local results = m_cargo.query(
        {'areas'},
        {'areas.infobox_html', 'areas.mainpage_categories'},
        {
            where=tpl_args.where,
            orderBy=tpl_args.order_by,
        }
    )
    
    local out = {}
    local cats = {}
    
    for _, row in ipairs(results) do
        out[#out+1] = row['areas.infobox_html']
        if row['areas.mainpage_categories'] ~= '' then
            for _, cat in ipairs(m_util.string.split(row['areas.mainpage_categories'], ',')) do
                cats[#cats+1] = string.gsub(cat, '[Cc]ategory:', '')
            end
        end
    end
    if tpl_args.cats then
        return table.concat(out) .. m_util.misc.add_category(cats)
    else
        return table.concat(out)
    end
end

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

local p = {}

p.table_areas = m_cargo.declare_factory{data=argument_map}

--
-- Template:Area
-- 
p.area = m_util.misc.invoker_factory(_area, {
    wrappers = cfg.wrappers.area,
})

--
-- Template:Query area infoboxes
-- 
p.query_area_info = m_util.misc.invoker_factory(_query_area_info, {
    wrappers = cfg.wrappers.query_area_info,
})

return p