Module:Navboxes: Difference between revisions

From Desynced Wiki
(Created page with "--[[ This module is about creating navboxes autopopulated with in game data, with game extra user customization on top of it. Table generated try to mimic the game data & categories. Extra "fake" categories or forced categorisation can be done by registering objects with the NavCategory template on the specific pages --]] local p = {} local cargo = mw.ext.cargo local USER_NAV_CATEGORIES_TABLE = "userNavCategories" ---@class TypeData ---@comment Contains categoryfilte...")
 
(= p.createNavBox(frame, "Buildings", "Building"))
Line 3: Line 3:
Table generated try to mimic the game data & categories.
Table generated try to mimic the game data & categories.
Extra "fake" categories or forced categorisation can be done by registering objects with the NavCategory template on the specific pages
Extra "fake" categories or forced categorisation can be done by registering objects with the NavCategory template on the specific pages
Usage:
There is only one public function here: createNavBox(<nav box title>, <navBoxType>)
Where navBoxType is within NavboxType values below.
--]]
--]]


local p = {}
local p = {}
local nav = {} -- you can use this one for debugging without exposing functions, return this instead of p
---@type any
mw = mw


local cargo = mw.ext.cargo
local cargo = mw.ext.cargo


local CATEGORY_FILTER_TABLE = "categoryfilter"
local USER_NAV_CATEGORIES_TABLE = "userNavCategories"
local USER_NAV_CATEGORIES_TABLE = "userNavCategories"


Line 16: Line 25:
---@field tab string
---@field tab string
---@field filterField string
---@field filterField string
---@field recipeType string
---@field recipeType string|nil
 
---@alias NavboxType "UNIT" | "BUILDING" | "COMPONENT" | "ITEM"


-- Possible types to create a navbox with
-- Possible types to create a navbox with
---@type table<string, TypeData>
---@type table<NavboxType, TypeData>
local TYPES = {
local TYPES = {
   UNIT = {
   UNIT = {
Line 53: Line 64:
---@param typeData TypeData
---@param typeData TypeData
---@return table<string, CategoryData>
---@return table<string, CategoryData>
local function queryBaseCategories(typeData)
nav.queryBaseCategories = function (typeData)
   local categoriesMeta = cargo.query(
   local categoriesMeta = cargo.query(
     'categoryfilter=cat',
     CATEGORY_FILTER_TABLE .. "=cat",
     { 'name', 'filterVal', 'ordering' },
     'name, filterVal, ordering',
     {
     {
       where = string.format('tab="%s" AND filterField="%s"', typeData.tab, typeData.filterField),
       where = string.format('tab="%s" AND filterField="%s"', typeData.tab, typeData.filterField),
Line 72: Line 83:
     local foundObjects = cargo.query(
     local foundObjects = cargo.query(
       typeData.cargo_table,
       typeData.cargo_table,
       { 'name' },
       'name',
       {
       {
         where = wherePart,
         where = wherePart,
Line 89: Line 100:
---@param type string
---@param type string
---@return table<string, CategoryData>
---@return table<string, CategoryData>
local function queryUserExtrasCategories(type)
nav.queryUserExtrasCategories =  function(type)
   local userCategoriesRows = cargo.query(
   local userCategoriesRows = cargo.query(
     USER_NAV_CATEGORIES_TABLE,
     USER_NAV_CATEGORIES_TABLE,
     {
     'category=catName, pagename',
      'category AS catName',
      'pagename'
    },
     {
     {
       where = string.format('UPPER(type) = "%s"', type)
       where = string.format('UPPER(type) = "%s"', type)
Line 118: Line 126:
---@param extra table<string, CategoryData>
---@param extra table<string, CategoryData>
---@return table<string, CategoryData>
---@return table<string, CategoryData>
local function mergeCategories(base, extra)
nav.mergeCategories = function (base, extra)
   -- Step 1: Remove from base any name also found in extra
   -- Step 1: Remove from base any name also found in extra
   for _, extraCat in pairs(extra) do
   for _, extraCat in pairs(extra) do
Line 153: Line 161:
---@param categories table<string, CategoryData>
---@param categories table<string, CategoryData>
---@return SortedCategoryEntry[]
---@return SortedCategoryEntry[]
local function sortCategories(categories)
nav.sortCategories = function(categories)
   -- Convert map to array
   -- Convert map to array
   local arr = {}
   local arr = {}
Line 172: Line 180:
-- @param {string} frame.args.title Navtable title
-- @param {string} frame.args.title Navtable title
-- @param {string} frame.args.type from types above here, like Building or Item (any case)
-- @param {string} frame.args.type from types above here, like Building or Item (any case)
---@param tableTitle string Navtable title
---@param rawType string from types above here, like Building or Item (any case)
---@return string
---@return string
p.createNavBox = function(frame)
nav.createNavBox = function(frame, tableTitle, rawType)
  local tableTitle = frame.args[1]
   ---@type NavboxType
  local rawType = frame.args[2]
  if not tableTitle then
    return "Navbox error: No tableTitle provided"
  end
  if not rawType then
    return "Navbox error: No type provided"
  end
   ---@type string
   local type = mw.ustring.upper(rawType or "")
   local type = mw.ustring.upper(rawType or "")
   local typeData = TYPES[type]
   local typeData = TYPES[type]
Line 189: Line 191:
   end
   end


   local baseCategories = queryBaseCategories(typeData)
   local baseCategories = nav.queryBaseCategories(typeData)
   local extraCategories = queryUserExtrasCategories(type)
   local extraCategories = nav.queryUserExtrasCategories(type)


   local categories = mergeCategories(baseCategories, extraCategories)
   local categories = nav.mergeCategories(baseCategories, extraCategories)
   local sortedCategories = sortCategories(categories)
  mw.logObject(categories)  
   local sortedCategories = nav.sortCategories(categories)


   local rowsHtml = ""
   local rowsHtml = ""
Line 225: Line 228:
-- NavboxCategoryItems, NavRows, NavTableCategory, ...
-- NavboxCategoryItems, NavRows, NavTableCategory, ...


return p
--- Build a navbox
-- @param {table} frame current frame
-- @param {string} frame.args.title Navtable title
-- @param {string} frame.args.type from types above here, like Building or Item (any case)
---@return string
p.createNavBox = function(frame)
  local tableTitle = frame.args[1]
  local rawType = frame.args[2]
  if not tableTitle then
    return "(Navbox error: No tableTitle provided)"
  end
  if not rawType then
    return "(Navbox error: No type provided)"
  end
  return nav.createNavBox(frame, tableTitle, rawType)
end
 
return nav
 
 


--[[
--[[
Debugging:
Return nav instead of p in the end to expose the internal function
= p.createNavBox(frame, "Buildings", "Building")
Resources:
Resources:
https://www.mediawiki.org/wiki/Extension:Cargo/Querying_data
https://www.mediawiki.org/wiki/Extension:Cargo/Querying_data


--]]
--]]

Revision as of 01:34, 6 August 2025

Description

This module is about creating navboxes autopopulated with in game data, with game extra user customization on top of it.
For Buildings & Items: The generated table tries to mimic the game data & categories.
For Bots: Using custom sorting defined in this script.
For Components: ?
Tech is not covered by this module.

Extra "fake" categories or forced categorisation can be done by registering objects with the Template:NavCategory on the specific pages.

Usage

Usage: {{#invoke:Navboxes|create|title=<NavboxTitle>|type=<navBoxType>}}
Where navBoxType is within NavboxType values, see LUA source in this page. As of writing, valid values are: Unit, Building, Component, Item (Case insensitive)

Example

Buildings

{{#invoke:Navboxes|create|title=Buildings|type=building}}

Script error: The function "create" does not exist.

Items

{{#invoke:Navboxes|create|title=Items|type=item}}

Script error: The function "create" does not exist.

Bots

{{#invoke:Navboxes|create|title=Bots|type=bot}}

Script error: The function "create" does not exist.

Components

{{#invoke:Navboxes|create|title=Components|type=component}}

Script error: The function "create" does not exist.


--[[
This module is about creating navboxes autopopulated with in game data, with game extra user customization on top of it.
Table generated try to mimic the game data & categories.
Extra "fake" categories or forced categorisation can be done by registering objects with the NavCategory template on the specific pages

Usage:
There is only one public function here: createNavBox(<nav box title>, <navBoxType>)
Where navBoxType is within NavboxType values below.
--]]

local p = {}
local nav = {} -- you can use this one for debugging without exposing functions, return this instead of p

---@type any
mw = mw

local cargo = mw.ext.cargo

local CATEGORY_FILTER_TABLE = "categoryfilter"
local USER_NAV_CATEGORIES_TABLE = "userNavCategories"

---@class TypeData
---@comment Contains categoryfilter selectors for given type
---@field cargo_table string
---@field tab string
---@field filterField string
---@field recipeType string|nil

---@alias NavboxType "UNIT" | "BUILDING" | "COMPONENT" | "ITEM"

-- Possible types to create a navbox with
---@type table<NavboxType, TypeData>
local TYPES = {
  UNIT = {
    cargo_table = "entity",
    tab = "frame",
    filterField = "size",
    recipeType = "Production"
  },
  BUILDING = {
    cargo_table = "entity",
    tab = "frame",
    filterField = "size",
    recipeType = "Construction",
  },
  COMPONENT = {
    cargo_table = "component",
    tab = "item",
    filterField = "attachment_size",
    recipeType = nil --unused
  },
  ITEM = {
    cargo_table = "item",
    tab = "item",
    filterField = "tag",
    recipeType = nil --unused
  }
}

---@class CategoryData
---@field ordering number?
---@field names string[]

---@param typeData TypeData
---@return table<string, CategoryData>
nav.queryBaseCategories = function (typeData)
  local categoriesMeta = cargo.query(
    CATEGORY_FILTER_TABLE .. "=cat",
    'name, filterVal, ordering',
    {
      where = string.format('tab="%s" AND filterField="%s"', typeData.tab, typeData.filterField),
      groupBy = 'cat.filterVal'
    }
  )
  local categories = {}
  for _, cat in ipairs(categoriesMeta) do
    local name, val, order = cat.name, cat.filterVal, cat.ordering
    local wherePart = string.format('%s="%s"', typeData.filterField, val)
    if typeData.recipeType then
      wherePart = wherePart .. string.format(' AND recipeType="%s"', typeData.recipeType)
    end

    local foundObjects = cargo.query(
      typeData.cargo_table,
      'name',
      {
        where = wherePart,
      }
    )
    local foundNames = {}
    for _, row in ipairs(foundObjects) do
      table.insert(foundNames, row.name)
    end
    categories[name] = { ordering = order, names = foundNames }
  end

  return categories
end

---@param type string
---@return table<string, CategoryData>
nav.queryUserExtrasCategories =  function(type)
  local userCategoriesRows = cargo.query(
    USER_NAV_CATEGORIES_TABLE,
    'category=catName, pagename',
    {
      where = string.format('UPPER(type) = "%s"', type)
    }
  )
  local categories = {}
  for _, row in ipairs(userCategoriesRows) do
    local catName = row.catName;
    if not categories[catName] then
      categories[catName] = { ordering = 999, names = {} }
    end

    table.insert(categories[catName].names, row.pagename)
  end

  return categories
end

-- Names in extra have priority.
-- Does mutate base
---@param base table<string, CategoryData>
---@param extra table<string, CategoryData>
---@return table<string, CategoryData>
nav.mergeCategories = function (base, extra)
  -- Step 1: Remove from base any name also found in extra
  for _, extraCat in pairs(extra) do
    for _, name in ipairs(extraCat.names) do
      for _, baseCat in pairs(base) do
        for i = #baseCat.names, 1, -1 do -- reverse loop to allow deletion while looping
          if baseCat.names[i] == name then
            table.remove(baseCat.names, i)
          end
        end
      end
    end
  end

  -- Step 2: Merge extra into base
  for catName, extraCat in pairs(extra) do
    if not base[catName] then
      base[catName] = extraCat
    else
      local target = base[catName].names
      for _, name in ipairs(extraCat.names) do
        table.insert(target, name)
      end
    end
  end

  return base
end

---@class SortedCategoryEntry
---@field name string
---@field data CategoryData

---@param categories table<string, CategoryData>
---@return SortedCategoryEntry[]
nav.sortCategories = function(categories)
  -- Convert map to array
  local arr = {}
  for name, data in pairs(categories) do
    table.insert(arr, { name = name, data = data })
  end

  -- Sort by ordering (ascending)
  table.sort(arr, function(a, b)
    return (a.data.ordering) < (b.data.ordering)
  end)

  return arr
end

--- Build a navbox
-- @param {table} frame current frame
-- @param {string} frame.args.title Navtable title
-- @param {string} frame.args.type from types above here, like Building or Item (any case)
---@param tableTitle string Navtable title
---@param rawType string from types above here, like Building or Item (any case)
---@return string
nav.createNavBox = function(frame, tableTitle, rawType)
  ---@type NavboxType
  local type = mw.ustring.upper(rawType or "")
  local typeData = TYPES[type]
  if not typeData then
    return string.format("Navbox error: Unknown type: '%s'", type)
  end

  local baseCategories = nav.queryBaseCategories(typeData)
  local extraCategories = nav.queryUserExtrasCategories(type)

  local categories = nav.mergeCategories(baseCategories, extraCategories)
  mw.logObject(categories) 
  local sortedCategories = nav.sortCategories(categories)

  local rowsHtml = ""
  for _, cat in pairs(sortedCategories) do
    local objects = "" -- list of objects to insert in a row
    for _, name in ipairs(cat.data.names) do
      objects = objects .. frame:expandTemplate { title = "NavboxIconLink2", args = { name = name } }
    end
    -- Returns a table header + a table row
    rowsHtml = rowsHtml .. frame:expandTemplate {
      title = "NavTableCategory",
      args = {
        tableTitle = tableTitle,
        catName = cat.name,
        objects = objects
      }
    }
  end

  -- Final table
  return frame:expandTemplate {
    title = "NavTable",
    args = {
      title = tableTitle,
      rows = rowsHtml
    }
  }
end

-- Cleanup existing templates after we're done, don't need the query ones, we only want the formatting ones
-- NavboxCategoryItems, NavRows, NavTableCategory, ...

--- Build a navbox
-- @param {table} frame current frame
-- @param {string} frame.args.title Navtable title
-- @param {string} frame.args.type from types above here, like Building or Item (any case)
---@return string
p.createNavBox = function(frame)
  local tableTitle = frame.args[1]
  local rawType = frame.args[2]
  if not tableTitle then
    return "(Navbox error: No tableTitle provided)"
  end
  if not rawType then
    return "(Navbox error: No type provided)"
  end
  return nav.createNavBox(frame, tableTitle, rawType)
end

return nav



--[[
Debugging:
Return nav instead of p in the end to expose the internal function 
= p.createNavBox(frame, "Buildings", "Building")

Resources:
https://www.mediawiki.org/wiki/Extension:Cargo/Querying_data

--]]