Module:Engineers: Difference between revisions

Current answers. Practical procedures. Reliable reference.
Jump to navigation Jump to search
Repair content model (previous save incorrectly wrote this as wikitext) + add p.count()
No edit summary
Line 1: Line 1:
-- Module:Engineers
-- Module:Engineers
-- Reads canonical Engineer facts from Module:Data/Engineers/<Name> (JSON
-- Reads canonical Engineer facts from Module:Data/Engineers/<Name> (JSON
-- content-model pages) and formats them for three consumers: an Engineer's
-- content-model pages) and formats them for multiple consumers:
-- own infobox (via Template:Engineer infobox), List of Engineers, and
-- Engineer Capability Comparison. One record, one module, three consumers
-- -- see Elite Dangerous Field Manual:Adding Structured Data.
--
--
-- Adding a new engineer: create Module:Data/Engineers/<Name> in the same
--  * Engineer infoboxes via Template:Engineer infobox
-- shape as the existing four, then append the name to
--  * List of Engineers
-- Module:Data/Engineers/index. Don't edit this module unless you're
--  * Engineer Capability Comparison
-- changing the schema itself.
--
-- This module adds appropriate internal wiki links when rendering content.
--
-- Adding a new Engineer:
--  1. Create Module:Data/Engineers/<Name>
--   2. Use the same JSON schema as the existing Engineer records
--  3. Append the Engineer name to Module:Data/Engineers/index
--
-- See Elite Dangerous Field Manual:Adding Structured Data.


local p = {}
local p = {}


-- Field order/labels shared by both the JSON-backed path and the manual
-- Field order and labels shared by both the structured-data path and the
-- fallback path, so {{Engineer infobox|system=...|facility=...}} produces
-- manual fallback path.
-- the same shape of infobox whether or not a JSON record exists yet.
local FIELD_ORDER = {
local FIELD_ORDER = {
{ key = 'engineer_type', label = 'Engineer type', group = 'ENGINEER' },
{ key = 'engineer_type', label = 'Engineer type', group = 'ENGINEER' },
Line 25: Line 29:
{ key = 'specialties', label = 'Specialties' },
{ key = 'specialties', label = 'Specialties' },
{ key = 'max_grade', label = 'Maximum grade' },
{ key = 'max_grade', label = 'Maximum grade' },
{ key = 'prerequisite_engineer', label = 'Prerequisite', group = 'ACCESS' },
{ key = 'prerequisite_engineer', label = 'Prerequisite', group = 'ACCESS' },
{ key = 'discovery_requirement', label = 'Discovery' },
{ key = 'discovery_requirement', label = 'Discovery' },
Line 30: Line 35:
{ key = 'unlock_requirement', label = 'Unlock' },
{ key = 'unlock_requirement', label = 'Unlock' },
}
}
------------------------------------------------------------------------
-- Utility functions
------------------------------------------------------------------------
-- Converts a plain structured-data value into an internal wiki link.
--
-- Examples:
--
--  wikilink('Deciat')
--    -> [[Deciat]]
--
--  wikilink('Farseer Inc')
--    -> [[Farseer Inc]]
--
-- Existing wiki links are left untouched so explicit/manual overrides
-- remain safe.
local function wikilink(value, target)
if value == nil or value == '' then
return value
end
if type(value) ~= 'string' then
return value
end
-- Already contains a wiki link.
if value:find('%[%[') then
return value
end
target = target or value
if target == value then
return '[[' .. value .. ']]'
end
return '[[' .. target .. '|' .. value .. ']]'
end
------------------------------------------------------------------------
-- Data loading
------------------------------------------------------------------------


local function loadRecord(name)
local function loadRecord(name)
local title = mw.title.new('Data/Engineers/' .. name, 'Module')
local title = mw.title.new('Data/Engineers/' .. name, 'Module')
if not title then
if not title then
return nil
return nil
end
end
local content = title:getContent()
local content = title:getContent()
if not content then
if not content then
return nil
return nil
end
end
local ok, data = pcall(mw.text.jsonDecode, content)
local ok, data = pcall(mw.text.jsonDecode, content)
if not ok then
if not ok then
return nil
return nil
end
end
return data
return data
end
end


local function loadIndex()
local function loadIndex()
local title = mw.title.new('Data/Engineers/index', 'Module')
local title = mw.title.new('Data/Engineers/index', 'Module')
local content = title and title:getContent()
local content = title and title:getContent()
if not content then
if not content then
return {}
return {}
end
end
local ok, data = pcall(mw.text.jsonDecode, content)
local ok, data = pcall(mw.text.jsonDecode, content)
if not ok then
if not ok then
return {}
return {}
end
end
return data
return data
end
end


local function joinSpecialties(d)
 
if d.specialties and type(d.specialties) == 'table' then
------------------------------------------------------------------------
return table.concat(d.specialties, ', ')
-- Specialty formatting
------------------------------------------------------------------------
 
-- Structured Engineer records should preferably store specialties as an
-- array:
--
-- "specialties": [
--    "Frame Shift Drive",
--    "Power Plant",
--    "Thrusters"
-- ]
--
-- This allows each module name to become its own internal link.
local function joinSpecialties(d, linked)
if not d or not d.specialties then
return nil
end
 
if type(d.specialties) == 'table' then
local values = {}
 
for _, specialty in ipairs(d.specialties) do
if linked then
table.insert(values, wikilink(specialty))
else
table.insert(values, specialty)
end
end
 
return table.concat(values, ', ')
end
end
-- Preserve legacy/string values unchanged. These may contain descriptive
-- prose rather than a single module name, so automatically linking the
-- entire string would not always be appropriate.
return d.specialties
return d.specialties
end
end


-- Builds a plain { key = displayValue } table from either a JSON record
 
-- or manually-supplied template args, using the same FIELD_ORDER either
------------------------------------------------------------------------
-- way.
-- Unlock formatting
------------------------------------------------------------------------
 
-- Supports a newer structured unlock format while retaining compatibility
-- with the existing unlock_requirement prose field.
--
-- Example JSON:
--
-- "unlock": {
--    "action": "Deliver",
--    "quantity": 1,
--    "item": "Meta-Alloys"
-- }
--
-- Produces:
--
-- Deliver 1 unit of [[Meta-Alloys]]
--
-- Optional fields:
--
-- "unit": "unit"
-- "unit_plural": "units"
-- "item_target": "Meta-Alloys"
--
-- For unlock requirements that do not fit this structure cleanly, continue
-- using unlock_requirement as a display string.
local function formatUnlock(d)
if not d then
return nil
end
 
if type(d.unlock) == 'table' and d.unlock.item then
local action = d.unlock.action or 'Deliver'
local quantity = d.unlock.quantity or 1
local quantityNumber = tonumber(quantity)
 
local singularUnit = d.unlock.unit or 'unit'
local pluralUnit = d.unlock.unit_plural or (singularUnit .. 's')
 
local unit
 
if quantityNumber == 1 then
unit = singularUnit
else
unit = pluralUnit
end
 
local item = wikilink(
d.unlock.item,
d.unlock.item_target or d.unlock.item
)
 
return string.format(
'%s %s %s of %s',
action,
tostring(quantity),
unit,
item
)
end
 
-- Backward compatibility with existing records.
return d.unlock_requirement
end
 
 
------------------------------------------------------------------------
-- Field construction
------------------------------------------------------------------------
 
-- Builds the display values used by an Engineer infobox.
--
-- Structured data supplies the defaults. Explicit parameters supplied by
-- an article override those defaults.
local function buildFields(d, pargs)
local function buildFields(d, pargs)
local fields = {}
local fields = {}
if d then
if d then
-- Start with the canonical values.
for _, f in ipairs(FIELD_ORDER) do
for _, f in ipairs(FIELD_ORDER) do
fields[f.key] = d[f.key]
fields[f.key] = d[f.key]
end
end
fields.specialties = joinSpecialties(d)
 
--------------------------------------------------------------------
-- Automatically linked structured fields
--------------------------------------------------------------------
 
fields.system = wikilink(d.system)
fields.body = wikilink(d.body)
fields.facility = wikilink(d.facility)
fields.region = wikilink(d.region)
fields.allegiance = wikilink(d.allegiance)
 
-- Each specialty/module gets its own internal link.
fields.specialties = joinSpecialties(d, true)
 
--------------------------------------------------------------------
-- Prerequisite Engineer
--------------------------------------------------------------------
 
if d.prerequisite_engineer then
if d.prerequisite_engineer then
local note = d.prerequisite_engineer_note and (' ' .. d.prerequisite_engineer_note) or ''
local note = ''
fields.prerequisite_engineer = '[[' .. d.prerequisite_engineer .. ']]' .. note
 
if d.prerequisite_engineer_note
and d.prerequisite_engineer_note ~= '' then
note = ' ' .. d.prerequisite_engineer_note
end
 
fields.prerequisite_engineer =
wikilink(d.prerequisite_engineer) .. note
else
else
fields.prerequisite_engineer = d.prerequisite_engineer_display or 'None — available from the start'
fields.prerequisite_engineer =
d.prerequisite_engineer_display
or 'None — available from the start'
end
end
--------------------------------------------------------------------
-- Unlock
--------------------------------------------------------------------
fields.unlock_requirement = formatUnlock(d)
else
else
-- No structured record exists yet. Fall back entirely to manually
-- supplied template parameters.
for _, f in ipairs(FIELD_ORDER) do
for _, f in ipairs(FIELD_ORDER) do
fields[f.key] = pargs[f.key]
fields[f.key] = pargs[f.key]
Line 89: Line 293:
end
end


-- Infobox pages may override the structured data directly in the article
------------------------------------------------------------------------
-- template call. This keeps the sidebar editable from page edit/source
-- Explicit article overrides
-- while still letting JSON records provide defaults and power comparison
------------------------------------------------------------------------
-- tables. Empty parameters do not erase structured defaults.
--
-- This preserves:
--
-- {{Engineer infobox
--  |image = DifferentPortrait.png
-- }}
--
-- as a valid way to override one structured value without duplicating
-- the rest of the Engineer record.
--
-- Empty parameters do not erase structured defaults.
for _, f in ipairs(FIELD_ORDER) do
for _, f in ipairs(FIELD_ORDER) do
if pargs[f.key] ~= nil and pargs[f.key] ~= '' then
if pargs[f.key] ~= nil and pargs[f.key] ~= '' then
Line 102: Line 316:
end
end


-- {{#invoke:Engineers|infobox}}, called from Template:Engineer infobox.
 
-- Falls back to whatever manual named params were passed if no JSON
------------------------------------------------------------------------
-- record exists yet, so the wrapper template is safe to adopt one page at
-- Engineer infobox
-- a time rather than needing every engineer's data backfilled first.
------------------------------------------------------------------------
 
-- {{#invoke:Engineers|infobox}}
--
-- Called by Template:Engineer infobox.
--
-- If a canonical JSON record exists, it supplies the defaults.
-- Explicit template parameters override structured values.
-- If no JSON record exists, manual parameters are used instead.
function p.infobox(frame)
function p.infobox(frame)
local parent = frame:getParent()
local parent = frame:getParent()
local pargs = parent and parent.args or {}
local pargs = parent and parent.args or {}
local name = pargs.name or mw.title.getCurrentTitle().text
local name = pargs.name or mw.title.getCurrentTitle().text
local d = loadRecord(name)
local d = loadRecord(name)
local fields = buildFields(d, pargs)
local fields = buildFields(d, pargs)
------------------------------------------------------------------------
-- Image
------------------------------------------------------------------------


local image = pargs.image
local image = pargs.image
if ( image == nil or image == '' ) and d and d.image then
 
if (image == nil or image == '')
and d
and d.image
and d.image ~= '' then
image = d.image
image = d.image
end
end


local args = { name = name, image = image }
------------------------------------------------------------------------
-- Build Template:Infobox parameters
------------------------------------------------------------------------
 
local args = {
name = name,
image = image
}
 
local n = 0
local n = 0
for _, f in ipairs(FIELD_ORDER) do
for _, f in ipairs(FIELD_ORDER) do
local value = fields[f.key]
local value = fields[f.key]
if value ~= nil and value ~= '' then
if value ~= nil and value ~= '' then
n = n + 1
n = n + 1
if f.group then
if f.group then
args['group' .. n] = f.group
args['group' .. n] = f.group
end
end
args['label' .. n] = f.label
args['label' .. n] = f.label
args['row' .. n] = value
args['row' .. n] = value
Line 132: Line 376:
end
end


return frame:expandTemplate{ title = 'Infobox', args = args }
return frame:expandTemplate {
title = 'Infobox',
args = args
}
end
end


-- {{#invoke:Engineers|list}}, used by List of Engineers.
 
------------------------------------------------------------------------
-- Engineer list
------------------------------------------------------------------------
 
-- {{#invoke:Engineers|list}}
--
-- Generates a sortable Engineer table from the canonical index.
--
-- Engineer names, systems, and specialties are linked automatically.
function p.list()
function p.list()
local rows = {}
local rows = {}
for _, name in ipairs(loadIndex()) do
for _, name in ipairs(loadIndex()) do
local d = loadRecord(name)
local d = loadRecord(name)
if d then
if d then
table.insert(rows, string.format(
table.insert(
'|-\n| [[%s]] || %s || %s || %s',
rows,
d.name,
string.format(
d.system or '—',
'|-\n| %s || %s || %s || %s',
joinSpecialties(d) or '—',
wikilink(d.name or name),
d.max_grade or 'Not yet confirmed'
wikilink(d.system) or '—',
))
joinSpecialties(d, true) or '—',
d.max_grade or 'Not yet confirmed'
)
)
end
end
end
end
return '{| class="wikitable sortable"\n! Engineer !! System !! Specialties !! Maximum grade\n'
 
.. table.concat(rows, '\n') .. '\n|}'
return
'{| class="wikitable sortable"\n'
.. '! Engineer !! System !! Specialties !! Maximum grade\n'
.. table.concat(rows, '\n')
.. '\n|}'
end
end


-- {{#invoke:Engineers|comparison}}, used by Engineer Capability Comparison.
 
------------------------------------------------------------------------
-- Engineer capability comparison
------------------------------------------------------------------------
 
-- {{#invoke:Engineers|comparison}}
--
-- Generates a comparison table from canonical Engineer data.
--
-- Engineer names, specialties, prerequisite Engineers, and structured
-- unlock items are linked automatically.
function p.comparison()
function p.comparison()
local rows = {}
local rows = {}
for _, name in ipairs(loadIndex()) do
for _, name in ipairs(loadIndex()) do
local d = loadRecord(name)
local d = loadRecord(name)
if d then
if d then
local prereq = d.prerequisite_engineer and ('[[' .. d.prerequisite_engineer .. ']]') or ( d.prerequisite_engineer_display or 'None' )
local prereq
table.insert(rows, string.format(
 
'|-\n| [[%s]] || %s || %s || %s',
if d.prerequisite_engineer then
d.name,
prereq = wikilink(d.prerequisite_engineer)
joinSpecialties(d) or '—',
else
prereq,
prereq =
d.unlock_requirement or 'Not yet confirmed'
d.prerequisite_engineer_display
))
or 'None'
end
 
table.insert(
rows,
string.format(
'|-\n| %s || %s || %s || %s',
wikilink(d.name or name),
joinSpecialties(d, true) or '—',
prereq,
formatUnlock(d) or 'Not yet confirmed'
)
)
end
end
end
end
return '{| class="wikitable sortable"\n! Engineer !! Specialties !! Prerequisite Engineer !! Unlock requirement\n'
 
.. table.concat(rows, '\n') .. '\n|}'
return
'{| class="wikitable sortable"\n'
.. '! Engineer !! Specialties !! Prerequisite Engineer !! Unlock requirement\n'
.. table.concat(rows, '\n')
.. '\n|}'
end
end


-- {{#invoke:Engineers|count}}, returns how many Engineers currently have a
 
-- canonical JSON record in Module:Data/Engineers/index -- used so pages that
------------------------------------------------------------------------
-- cite this number (e.g. List of Engineers' {{Article status}} uncertainty
-- Engineer count
-- text) stay correct automatically as records are added, instead of a
------------------------------------------------------------------------
-- hand-typed number silently going stale.
 
-- {{#invoke:Engineers|count}}
--
-- Returns the number of canonical Engineer records currently present in
-- Module:Data/Engineers/index.
function p.count()
function p.count()
return tostring(#loadIndex())
return tostring(#loadIndex())
end
end


return p
return p

Revision as of 20:26, 19 August 2026

Documentation for this module may be created at Module:Engineers/doc

-- Module:Engineers
-- Reads canonical Engineer facts from Module:Data/Engineers/<Name> (JSON
-- content-model pages) and formats them for multiple consumers:
--
--   * Engineer infoboxes via Template:Engineer infobox
--   * List of Engineers
--   * Engineer Capability Comparison
--
-- This module adds appropriate internal wiki links when rendering content.
--
-- Adding a new Engineer:
--   1. Create Module:Data/Engineers/<Name>
--   2. Use the same JSON schema as the existing Engineer records
--   3. Append the Engineer name to Module:Data/Engineers/index
--
-- See Elite Dangerous Field Manual:Adding Structured Data.

local p = {}

-- Field order and labels shared by both the structured-data path and the
-- manual fallback path.
local FIELD_ORDER = {
	{ key = 'engineer_type', label = 'Engineer type', group = 'ENGINEER' },
	{ key = 'system', label = 'System' },
	{ key = 'body', label = 'Body' },
	{ key = 'facility', label = 'Facility' },
	{ key = 'region', label = 'Region' },
	{ key = 'allegiance', label = 'Allegiance' },
	{ key = 'specialties', label = 'Specialties' },
	{ key = 'max_grade', label = 'Maximum grade' },

	{ key = 'prerequisite_engineer', label = 'Prerequisite', group = 'ACCESS' },
	{ key = 'discovery_requirement', label = 'Discovery' },
	{ key = 'invitation_requirement', label = 'Invitation' },
	{ key = 'unlock_requirement', label = 'Unlock' },
}


------------------------------------------------------------------------
-- Utility functions
------------------------------------------------------------------------

-- Converts a plain structured-data value into an internal wiki link.
--
-- Examples:
--
--   wikilink('Deciat')
--     -> [[Deciat]]
--
--   wikilink('Farseer Inc')
--     -> [[Farseer Inc]]
--
-- Existing wiki links are left untouched so explicit/manual overrides
-- remain safe.
local function wikilink(value, target)
	if value == nil or value == '' then
		return value
	end

	if type(value) ~= 'string' then
		return value
	end

	-- Already contains a wiki link.
	if value:find('%[%[') then
		return value
	end

	target = target or value

	if target == value then
		return '[[' .. value .. ']]'
	end

	return '[[' .. target .. '|' .. value .. ']]'
end


------------------------------------------------------------------------
-- Data loading
------------------------------------------------------------------------

local function loadRecord(name)
	local title = mw.title.new('Data/Engineers/' .. name, 'Module')

	if not title then
		return nil
	end

	local content = title:getContent()

	if not content then
		return nil
	end

	local ok, data = pcall(mw.text.jsonDecode, content)

	if not ok then
		return nil
	end

	return data
end


local function loadIndex()
	local title = mw.title.new('Data/Engineers/index', 'Module')
	local content = title and title:getContent()

	if not content then
		return {}
	end

	local ok, data = pcall(mw.text.jsonDecode, content)

	if not ok then
		return {}
	end

	return data
end


------------------------------------------------------------------------
-- Specialty formatting
------------------------------------------------------------------------

-- Structured Engineer records should preferably store specialties as an
-- array:
--
-- "specialties": [
--     "Frame Shift Drive",
--     "Power Plant",
--     "Thrusters"
-- ]
--
-- This allows each module name to become its own internal link.
local function joinSpecialties(d, linked)
	if not d or not d.specialties then
		return nil
	end

	if type(d.specialties) == 'table' then
		local values = {}

		for _, specialty in ipairs(d.specialties) do
			if linked then
				table.insert(values, wikilink(specialty))
			else
				table.insert(values, specialty)
			end
		end

		return table.concat(values, ', ')
	end

	-- Preserve legacy/string values unchanged. These may contain descriptive
	-- prose rather than a single module name, so automatically linking the
	-- entire string would not always be appropriate.
	return d.specialties
end


------------------------------------------------------------------------
-- Unlock formatting
------------------------------------------------------------------------

-- Supports a newer structured unlock format while retaining compatibility
-- with the existing unlock_requirement prose field.
--
-- Example JSON:
--
-- "unlock": {
--     "action": "Deliver",
--     "quantity": 1,
--     "item": "Meta-Alloys"
-- }
--
-- Produces:
--
-- Deliver 1 unit of [[Meta-Alloys]]
--
-- Optional fields:
--
-- "unit": "unit"
-- "unit_plural": "units"
-- "item_target": "Meta-Alloys"
--
-- For unlock requirements that do not fit this structure cleanly, continue
-- using unlock_requirement as a display string.
local function formatUnlock(d)
	if not d then
		return nil
	end

	if type(d.unlock) == 'table' and d.unlock.item then
		local action = d.unlock.action or 'Deliver'
		local quantity = d.unlock.quantity or 1
		local quantityNumber = tonumber(quantity)

		local singularUnit = d.unlock.unit or 'unit'
		local pluralUnit = d.unlock.unit_plural or (singularUnit .. 's')

		local unit

		if quantityNumber == 1 then
			unit = singularUnit
		else
			unit = pluralUnit
		end

		local item = wikilink(
			d.unlock.item,
			d.unlock.item_target or d.unlock.item
		)

		return string.format(
			'%s %s %s of %s',
			action,
			tostring(quantity),
			unit,
			item
		)
	end

	-- Backward compatibility with existing records.
	return d.unlock_requirement
end


------------------------------------------------------------------------
-- Field construction
------------------------------------------------------------------------

-- Builds the display values used by an Engineer infobox.
--
-- Structured data supplies the defaults. Explicit parameters supplied by
-- an article override those defaults.
local function buildFields(d, pargs)
	local fields = {}

	if d then
		-- Start with the canonical values.
		for _, f in ipairs(FIELD_ORDER) do
			fields[f.key] = d[f.key]
		end

		--------------------------------------------------------------------
		-- Automatically linked structured fields
		--------------------------------------------------------------------

		fields.system = wikilink(d.system)
		fields.body = wikilink(d.body)
		fields.facility = wikilink(d.facility)
		fields.region = wikilink(d.region)
		fields.allegiance = wikilink(d.allegiance)

		-- Each specialty/module gets its own internal link.
		fields.specialties = joinSpecialties(d, true)

		--------------------------------------------------------------------
		-- Prerequisite Engineer
		--------------------------------------------------------------------

		if d.prerequisite_engineer then
			local note = ''

			if d.prerequisite_engineer_note
				and d.prerequisite_engineer_note ~= '' then
				note = ' ' .. d.prerequisite_engineer_note
			end

			fields.prerequisite_engineer =
				wikilink(d.prerequisite_engineer) .. note
		else
			fields.prerequisite_engineer =
				d.prerequisite_engineer_display
				or 'None — available from the start'
		end

		--------------------------------------------------------------------
		-- Unlock
		--------------------------------------------------------------------

		fields.unlock_requirement = formatUnlock(d)

	else
		-- No structured record exists yet. Fall back entirely to manually
		-- supplied template parameters.
		for _, f in ipairs(FIELD_ORDER) do
			fields[f.key] = pargs[f.key]
		end
	end

	------------------------------------------------------------------------
	-- Explicit article overrides
	------------------------------------------------------------------------
	--
	-- This preserves:
	--
	-- {{Engineer infobox
	--  |image = DifferentPortrait.png
	-- }}
	--
	-- as a valid way to override one structured value without duplicating
	-- the rest of the Engineer record.
	--
	-- Empty parameters do not erase structured defaults.
	for _, f in ipairs(FIELD_ORDER) do
		if pargs[f.key] ~= nil and pargs[f.key] ~= '' then
			fields[f.key] = pargs[f.key]
		end
	end

	return fields
end


------------------------------------------------------------------------
-- Engineer infobox
------------------------------------------------------------------------

-- {{#invoke:Engineers|infobox}}
--
-- Called by Template:Engineer infobox.
--
-- If a canonical JSON record exists, it supplies the defaults.
-- Explicit template parameters override structured values.
-- If no JSON record exists, manual parameters are used instead.
function p.infobox(frame)
	local parent = frame:getParent()
	local pargs = parent and parent.args or {}

	local name = pargs.name or mw.title.getCurrentTitle().text
	local d = loadRecord(name)

	local fields = buildFields(d, pargs)

	------------------------------------------------------------------------
	-- Image
	------------------------------------------------------------------------

	local image = pargs.image

	if (image == nil or image == '')
		and d
		and d.image
		and d.image ~= '' then
		image = d.image
	end

	------------------------------------------------------------------------
	-- Build Template:Infobox parameters
	------------------------------------------------------------------------

	local args = {
		name = name,
		image = image
	}

	local n = 0

	for _, f in ipairs(FIELD_ORDER) do
		local value = fields[f.key]

		if value ~= nil and value ~= '' then
			n = n + 1

			if f.group then
				args['group' .. n] = f.group
			end

			args['label' .. n] = f.label
			args['row' .. n] = value
		end
	end

	return frame:expandTemplate {
		title = 'Infobox',
		args = args
	}
end


------------------------------------------------------------------------
-- Engineer list
------------------------------------------------------------------------

-- {{#invoke:Engineers|list}}
--
-- Generates a sortable Engineer table from the canonical index.
--
-- Engineer names, systems, and specialties are linked automatically.
function p.list()
	local rows = {}

	for _, name in ipairs(loadIndex()) do
		local d = loadRecord(name)

		if d then
			table.insert(
				rows,
				string.format(
					'|-\n| %s || %s || %s || %s',
					wikilink(d.name or name),
					wikilink(d.system) or '—',
					joinSpecialties(d, true) or '—',
					d.max_grade or 'Not yet confirmed'
				)
			)
		end
	end

	return
		'{| class="wikitable sortable"\n'
		.. '! Engineer !! System !! Specialties !! Maximum grade\n'
		.. table.concat(rows, '\n')
		.. '\n|}'
end


------------------------------------------------------------------------
-- Engineer capability comparison
------------------------------------------------------------------------

-- {{#invoke:Engineers|comparison}}
--
-- Generates a comparison table from canonical Engineer data.
--
-- Engineer names, specialties, prerequisite Engineers, and structured
-- unlock items are linked automatically.
function p.comparison()
	local rows = {}

	for _, name in ipairs(loadIndex()) do
		local d = loadRecord(name)

		if d then
			local prereq

			if d.prerequisite_engineer then
				prereq = wikilink(d.prerequisite_engineer)
			else
				prereq =
					d.prerequisite_engineer_display
					or 'None'
			end

			table.insert(
				rows,
				string.format(
					'|-\n| %s || %s || %s || %s',
					wikilink(d.name or name),
					joinSpecialties(d, true) or '—',
					prereq,
					formatUnlock(d) or 'Not yet confirmed'
				)
			)
		end
	end

	return
		'{| class="wikitable sortable"\n'
		.. '! Engineer !! Specialties !! Prerequisite Engineer !! Unlock requirement\n'
		.. table.concat(rows, '\n')
		.. '\n|}'
end


------------------------------------------------------------------------
-- Engineer count
------------------------------------------------------------------------

-- {{#invoke:Engineers|count}}
--
-- Returns the number of canonical Engineer records currently present in
-- Module:Data/Engineers/index.
function p.count()
	return tostring(#loadIndex())
end


return p