Module:Navboxes

From Desynced Wiki
Revision as of 01:34, 6 August 2025 by Kelno (talk | contribs) (= p.createNavBox(frame, "Buildings", "Building"))

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

--]]