Module:Navboxes: Difference between revisions

From Desynced Wiki
No edit summary
No edit summary
Tag: Manual revert
 
(10 intermediate revisions by the same user not shown)
Line 1: Line 1:
-- This file about generating the navboxes from the game file instead of adding objects manually in them.   
-- This file about generating the navboxes from the game file instead of adding objects manually in them.   
-- Write doc in Naboxes_doc.mediawiki, and update it on the wiki with https://wiki.desyncedgame.com/index.php?title=Module:Navboxes/doc&action=edit
-- Write doc on the wiki with https://wiki.desyncedgame.com/index.php?title=Module:Navboxes/doc&action=edit


local p = {}
local p = {}
Line 24: Line 24:
---@field filterField string
---@field filterField string
---@field recipeType string|nil
---@field recipeType string|nil
---@field orderBy string|nil


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


-- Possible types to create a navbox with
-- Possible types to create a navbox with
-- THe filter fields & recipe types are about listing & filtering like the game does
---@type table<NavboxType, TypeData>
---@type table<NavboxType, TypeData>
local TYPES = {
local TYPES = {
   BOT = {
   BOT = {
     cargo_table = "entity",
     cargo_table = "entity",
     extra_fields = "race, types1, movement_speed",
     extra_fields = "race, slotType",
     tab = "frame",
     tab = "frame",
     filterField = "size",
     filterField = "size",
Line 47: Line 49:
     tab = "item",
     tab = "item",
     filterField = "attachmentSize",
     filterField = "attachmentSize",
    recipeType = nil --unused
   },
   },
   ITEM = {
   ITEM = {
     cargo_table = "item",
     cargo_table = "item",
    extra_fields = "type, race",
     tab = "item",
     tab = "item",
     filterField = "tag",
     filterField = "tag",
     recipeType = nil --unused
     orderBy = "type, race"
   }
   }
}
}
Line 69: Line 71:
     end
     end
   end
   end
end
---@param str string
---@return string
m.capitalize_words = function(str)
    return (str:gsub("(%l)(%w*)", function(first, rest)
        return first:upper() .. rest
    end))
end
end


Line 98: Line 92:




---Custom sorting for bots here
---@param type NavboxType
---@param type NavboxType
---@param categories table<string, CategoryData>
---@param categories table<string, CategoryData>
Line 105: Line 100:
---@return nil
---@return nil
m.sortInCategory = function(type, categories, game_cat_name, ordering, row)
m.sortInCategory = function(type, categories, game_cat_name, ordering, row)
    
   if type == "BOT" then
  m.createOrAppendToCategory(categories, game_cat_name, row.name, ordering)
    if row.slotType ~= nil and mw.ustring.upper(row.slotType) == "DRONE" then
 
      m.createOrAppendToCategory(categories, "Drone", row.name, ordering)
  -- categories[game_cat_name] = { ordering = tonumber(ordering) or 0, names = foundNames }
    elseif row.slotType ~= nil and mw.ustring.upper(row.slotType) == "SATELLITE" then -- Doesn't actually get passed to this function atm
      m.createOrAppendToCategory(categories, "Satellite", row.name, ordering)
    elseif row.race ~= nil then
      m.createOrAppendToCategory(categories, row.race, row.name, ordering)
    else -- fallback
      m.createOrAppendToCategory(categories, "Other", row.name, ordering)
    end
  else
    m.createOrAppendToCategory(categories, game_cat_name, row.name, ordering)
  end
end


  -- for _, row in ipairs(foundObjects) do
---@param categories table<string, CategoryData>
 
---@return nil
  --   if row.types1 == "Building" and row.movement_speed > 0 then
m.addBugsBots = function(categories)
  --     m.createOrAppendToCategory(categories, "Moving Buildings", row.name, ordering)
  local typeData = TYPES["BOT"]
   --  else
  local query_objects = cargo.query(
  --     m.createOrAppendToCategory(categories, m.capitalize_words(row.race), row.name, ordering)
     typeData.cargo_table,
   --  end
    'name',
  -- end
    {
      where = ('size = "Unit" AND race = "Virus"'),
    }
  )
   for _, row in ipairs(query_objects) do
     m.createOrAppendToCategory(categories, "Bugs", row.name, 999)
   end
end
end


---@param type NavboxType
---@param type NavboxType
Line 141: Line 151:
     end
     end


     local fields = 'name'
     local fields = 'name, unlockable'
     if typeData.extra_fields ~= nil then
     if typeData.extra_fields ~= nil then
         fields = fields .. ',' .. typeData.extra_fields
         fields = fields .. ',' .. typeData.extra_fields
Line 150: Line 160:
       {
       {
         where = wherePart,
         where = wherePart,
        orderBy = typeData.orderBy
       }
       }
     )
     )
Line 155: Line 166:
       m.sortInCategory(type, categories, game_cat_name, tonumber(ordering) or 0, row)
       m.sortInCategory(type, categories, game_cat_name, tonumber(ordering) or 0, row)
     end
     end
  end
 
  if type == "BOT" then
    m.addBugsBots(categories)
   end
   end


Line 255: Line 271:
   local frame = mw.getCurrentFrame()
   local frame = mw.getCurrentFrame()
    
    
  ---@type table<string, CategoryData>
   local baseCategories = m.queryBaseCategories(type)
   local baseCategories
  if type == "BOT" then
    baseCategories = m.queryBotsCategories()
  else
    baseCategories = m.queryBaseCategories(type)
  end


   local extraCategories = m.queryUserExtrasCategories(type)
   local extraCategories = m.queryUserExtrasCategories(type)
Line 323: Line 333:


local frame = { args = { title = "Buildings", type = "Building" } }
local frame = { args = { title = "Buildings", type = "Building" } }
local frame = { args = { title = "Bots", type = "BOT" } }
(enter)
(enter)
= p.create(frame, "Buildings", "Building")
= p.create(frame)
(enter)
(enter)



Latest revision as of 06:37, 13 August 2025

Description[edit source]

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[edit source]

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[edit source]

Buildings[edit source]

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

Items[edit source]

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

Bots[edit source]

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

Components[edit source]

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


-- This file about generating the navboxes from the game file instead of adding objects manually in them.  
-- Write doc on the wiki with https://wiki.desyncedgame.com/index.php?title=Module:Navboxes/doc&action=edit

local p = {}
local m = {} -- Use for internal function. You can use this one for debugging without exposing functions, return this instead of p

---@type any
---@diagnostic disable-next-line: lowercase-global
mw = mw

local cargo = mw.ext.cargo

local CATEGORY_FILTER_TABLE = "categoryfilter"
local USER_NAV_CATEGORIES_TABLE = "userNavCategories"
local function stripWhitespace(str)
    return str:match("^%s*(.-)%s*$")
end

---@class TypeData
---@comment Contains categoryfilter selectors for given type
---@field cargo_table string
---@field extra_fields string|nil Fields to query, beside name
---@field tab string
---@field filterField string
---@field recipeType string|nil
---@field orderBy string|nil

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

-- Possible types to create a navbox with
-- THe filter fields & recipe types are about listing & filtering like the game does
---@type table<NavboxType, TypeData>
local TYPES = {
  BOT = {
    cargo_table = "entity",
    extra_fields = "race, slotType",
    tab = "frame",
    filterField = "size",
    recipeType = "Production"
  },
  BUILDING = {
    cargo_table = "entity",
    tab = "frame",
    filterField = "size",
    recipeType = "Construction",
  },
  COMPONENT = {
    cargo_table = "component",
    tab = "item",
    filterField = "attachmentSize",
  },
  ITEM = {
    cargo_table = "item",
    extra_fields = "type, race",
    tab = "item",
    filterField = "tag",
    orderBy = "type, race"
  }
}

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

---@param categories table<string, CategoryData>
---@return nil
m.removeEmptyCategories = function(categories)
 for k, v in pairs(categories) do
    if #(v.names) == 0 then
      categories[k] = nil
    end
  end
end

---@param categories table<string, CategoryData>
---@param cat_name string
---@param name string
---@param ordering number
---@return nil
m.createOrAppendToCategory = function(categories, cat_name, name, ordering)
   -- If the category doesn't exist, create it
    if not categories[cat_name] then
        categories[cat_name] = {
            ordering = ordering,
            names = {}
        }
    end

    -- Append the new name
    table.insert(categories[cat_name].names, name)
end


---Custom sorting for bots here
---@param type NavboxType
---@param categories table<string, CategoryData>
---@param game_cat_name string
---@param ordering number
---@param row table
---@return nil
m.sortInCategory = function(type, categories, game_cat_name, ordering, row)
  if type == "BOT" then
    if row.slotType ~= nil and mw.ustring.upper(row.slotType) == "DRONE" then
      m.createOrAppendToCategory(categories, "Drone", row.name, ordering)
    elseif row.slotType ~= nil and mw.ustring.upper(row.slotType) == "SATELLITE" then -- Doesn't actually get passed to this function atm
      m.createOrAppendToCategory(categories, "Satellite", row.name, ordering)
    elseif row.race ~= nil then
      m.createOrAppendToCategory(categories, row.race, row.name, ordering)
    else -- fallback
      m.createOrAppendToCategory(categories, "Other", row.name, ordering)
    end
  else
    m.createOrAppendToCategory(categories, game_cat_name, row.name, ordering)
  end
end

---@param categories table<string, CategoryData>
---@return nil
m.addBugsBots = function(categories)
  local typeData = TYPES["BOT"]
  local query_objects = cargo.query(
    typeData.cargo_table,
    'name',
    {
      where = ('size = "Unit" AND race = "Virus"'),
    }
  )
  for _, row in ipairs(query_objects) do
    m.createOrAppendToCategory(categories, "Bugs", row.name, 999)
  end
end

---@param type NavboxType
---@return table<string, CategoryData>
m.queryBaseCategories = function (type)
  local typeData = TYPES[type]
  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 game_cat_name, filter_val, ordering = cat.name, cat.filterVal, cat.ordering
    local wherePart = string.format('%s="%s" AND unlockable = TRUE', typeData.filterField, filter_val)
    if typeData.recipeType then
      wherePart = wherePart .. string.format(' AND recipeType="%s"', typeData.recipeType)
    end

    local fields = 'name, unlockable'
    if typeData.extra_fields ~= nil then
        fields = fields .. ',' .. typeData.extra_fields
    end
    local query_objects = cargo.query(
      typeData.cargo_table,
      fields,
      {
        where = wherePart,
        orderBy = typeData.orderBy
      }
    )
    for _, row in ipairs(query_objects) do
      m.sortInCategory(type, categories, game_cat_name, tonumber(ordering) or 0, row)
    end
  end

  
  if type == "BOT" then
    m.addBugsBots(categories)
  end

  return categories
end

---@param type NavboxType
---@return table<string, CategoryData>
m.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>
m.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[]
m.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
m.createNavBox = function(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 frame = mw.getCurrentFrame()
  
  local baseCategories = m.queryBaseCategories(type)

  local extraCategories = m.queryUserExtrasCategories(type)

  local categories = m.mergeCategories(baseCategories, extraCategories)
  -- mw.logObject(categories) 
  m.removeEmptyCategories(categories)

  local sortedCategories = m.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 .. stripWhitespace(frame:expandTemplate { title = "NavboxIconLinkNamed", args = { name = name } })
    end
    -- Returns a table row [ category | objects ]
    rowsHtml = rowsHtml .. stripWhitespace(frame:expandTemplate {
      title = "NavTableCategory",
      args = {
        tableTitle = tableTitle,
        catName = cat.name,
        objects = objects
      }
    })
  end

  -- Final table
  return stripWhitespace(frame:expandTemplate {
    title = "NavTable",
    args = {
      title = tableTitle,
      rows = rowsHtml
    }
  })
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)
---@return string
p.create = function(frame)
  local title = frame.args.title
  local rawType = frame.args.type
  if not title then
    return "(Navbox error: No title provided)"
  end
  if not rawType then
    return "(Navbox error: No type provided)"
  end
  return m.createNavBox(title, rawType)
end

return p



--[[
Debugging:

local frame = { args = { title = "Buildings", type = "Building" } }
local frame = { args = { title = "Bots", type = "BOT" } }
(enter)
= p.create(frame)
(enter)

Return m instead of p if you want to debug the non exposed functions

Resources:
General cargo: https://www.mediawiki.org/wiki/Extension:Cargo/Querying_data
Cargo querying from LUA: https://www.mediawiki.org/wiki/Extension:Cargo/Other_features#Lua_support
https://www.mediawiki.org/wiki/Extension:Scribunto/Lua_reference_manual

--]]