Jump to content
Toggle menu
  • 69 articles
  • 130 files
  • 3 users
  • 552 edits
The Ninja Warriors Again RX Wiki
Toggle preferences menu
Toggle personal menu
Not logged in
Your IP address will be publicly visible if you make any edits.

Module:GameData: Difference between revisions

From The Ninja Warriors Again RX Wiki
Game data from 7223a3d (demo build of v2.2)
Game data from 645555d
 
(5 intermediate revisions by the same user not shown)
Line 59: Line 59:
end
end


-- A link to a page, labelled with its name shown literally.
-- A name inside an infobox tag. PortableInfobox decodes character references before
-- parsing its content, so the apostrophes are kept in nowiki instead.
local function infoboxText( name )
return ( tostring( name ):gsub( "''", "<nowiki>''</nowiki>" ) )
end
 
-- The in-game name of each page whose title differs from it ("Arrowstorm (skill)" is
-- named "Arrowstorm"), written by the generator. Links show the name, not the title.
local namesLoaded, names = pcall( mw.loadData, 'Module:GameData/names' )
if not namesLoaded then
names = {}
end
 
-- A link to a page, labelled with its in-game name shown literally.
local function pageLink( title )
local function pageLink( title )
if tostring( title ):find( "''", 1, true ) then
local label = names[title] or title
return '[[' .. title .. '|' .. nameText( title ) .. ']]'
if label ~= title or tostring( label ):find( "''", 1, true ) then
return '[[' .. title .. '|' .. nameText( label ) .. ']]'
end
end
return '[[' .. title .. ']]'
return '[[' .. title .. ']]'
Line 159: Line 173:
local row = fetchRow( t, page )
local row = fetchRow( t, page )
if not row then
if not row then
return "<div class='gamedata-missing'>No game data for this page.</div>"
return ''
end
end
local out = { '<infobox>', '<title><default>' .. nameText( row.name or page ) .. '</default></title>' }
local out = { '<infobox>', '<title><default>' .. infoboxText( row.name or page ) .. '</default></title>' }
if row.icon and row.icon ~= '' then
if row.icon and row.icon ~= '' then
out[#out + 1] = '<image><default>' .. row.icon .. '</default></image>'
out[#out + 1] = '<image><default>' .. row.icon .. '</default></image>'
Line 168: Line 182:
if f.label then
if f.label then
local value = formatValue( f, row[f.name], row )
local value = formatValue( f, row[f.name], row )
-- Plain words start with a capital, as the game shows them ("Monster").
if value and f.type == 'String' and not f.link and not f.list then
value = mw.ustring.upper( mw.ustring.sub( value, 1, 1 ) ) .. mw.ustring.sub( value, 2 )
end
if value then
if value then
out[#out + 1] = '<data><label>' .. f.label .. '</label><default>' .. value .. '</default></data>'
out[#out + 1] = '<data><label>' .. f.label .. '</label><default>' .. value .. '</default></data>'
end
end
end
end
end
if row.game_version and row.game_version ~= '' then
out[#out + 1] = '<data><label>Game data from</label><default>' .. row.game_version .. '</default></data>'
end
end
out[#out + 1] = '</infobox>'
out[#out + 1] = '</infobox>'
Line 197: Line 212:
end
end
local groups, order = {}, {}
local groups, order = {}, {}
local shown = {}
for _, row in ipairs( rows ) do
-- A line with a slot for its value and a value of 0 does nothing: left out.
if not ( row.text:find( '{0}', 1, true ) and tonumber( row.value ) == 0 ) then
shown[#shown + 1] = row
end
end
if #shown == 0 then
return ''
end
rows = shown
for _, row in ipairs( rows ) do
for _, row in ipairs( rows ) do
local scaling = row.scaling or ''
local scaling = row.scaling or ''
Line 331: Line 357:
end )
end )
return frame:preprocess( text )
return frame:preprocess( text )
end
-- Gallery images fit a square box of this many pixels, scaled by a whole number (up by
-- the largest factor that fits, or down by the smallest divisor that fits), so pixel art
-- keeps square pixels.
local GALLERY_BOX = 96
local function galleryImage( file, page )
local title = mw.title.new( 'File:' .. file )
-- Reading a file's size is an "expensive" call, limited per page; past the limit the
-- image is fitted to the box by the browser instead.
local ok, info = pcall( function () return title and title.file end )
local width, height = ok and info and info.width, ok and info and info.height
if not width or width == 0 or height == 0 then
return '[[File:' .. file .. '|' .. GALLERY_BOX .. 'x' .. GALLERY_BOX .. 'px|link=' .. page .. ']]'
end
local side = math.max( width, height )
if side <= GALLERY_BOX then
local factor = math.floor( GALLERY_BOX / side )
width, height = width * factor, height * factor
else
local divisor = math.ceil( side / GALLERY_BOX )
width, height = math.floor( width / divisor ), math.floor( height / divisor )
end
return '<span class="gd-gallery-fit" style="width:' .. width .. 'px;height:' .. height .. 'px">[[File:'
.. file .. '|link=' .. page .. ']]</span>'
end
end


Line 342: Line 394:
local out = { '<div class="gd-gallery">' }
local out = { '<div class="gd-gallery">' }
for _, row in ipairs( rows ) do
for _, row in ipairs( rows ) do
local image = ( row.icon and row.icon ~= '' ) and ( '[[File:' .. row.icon .. '|96px|link=' .. row.wiki_page .. ']]<br>' ) or ''
local image = ''
if row.icon and row.icon ~= '' then
image = '<div class="gd-gallery-image">' .. galleryImage( row.icon, row.wiki_page ) .. '</div>'
end
out[#out + 1] = '<div class="gd-gallery-item">' .. image .. pageLink( row.wiki_page ) .. '</div>'
out[#out + 1] = '<div class="gd-gallery-item">' .. image .. pageLink( row.wiki_page ) .. '</div>'
end
end
Line 407: Line 462:
local out = { '== Modifiers ==', '{| class="wikitable gd-unique-mods"', '! Modifier !! Mild !! Spicy !! Inferno !! Nightmare' }
local out = { '== Modifiers ==', '{| class="wikitable gd-unique-mods"', '! Modifier !! Mild !! Spicy !! Inferno !! Nightmare' }
for _, mod in ipairs( mods ) do
for _, mod in ipairs( mods ) do
local text = colorize( withValue( mod.text or '', '#' ) )
-- The wording's slot shows the whole range across the difficulty columns.
local low, high
for _, value in ipairs( { mod.mild, mod.spicy, mod.inferno, mod.nightmare } ) do
for number in tostring( value or '' ):gmatch( '%-?%d+%.?%d*' ) do
number = tonumber( number )
if number then
low = low and math.min( low, number ) or number
high = high and math.max( high, number ) or number
end
end
end
local range = low and ( low == high and tostring( low ) or ( low .. '-' .. high ) ) or '#'
local text = colorize( withValue( mod.text or '', range ) )
if mod.implicit == '1' then
if mod.implicit == '1' then
text = text .. " <small>''(implicit)''</small>"
text = text .. " <small>''(implicit)''</small>"
Line 515: Line 582:
end
end
want( v.group )
want( v.group )
if v.gallery then
want( v.gallery.image )
want( v.gallery.caption )
end
local match = v.match or 'wiki_page'
local match = v.match or 'wiki_page'
local rows = mw.ext.cargo.query( v.table, table.concat( names, ',' ), {
local rows = mw.ext.cargo.query( v.table, table.concat( names, ',' ), {
Line 527: Line 598:
if v.note then
if v.note then
out[#out + 1] = "''" .. v.note .. "''"
out[#out + 1] = "''" .. v.note .. "''"
end
-- A view with "gallery" shows its rows as an image gallery: the file and caption fields.
if v.gallery then
local lines = {}
for _, row in ipairs( rows ) do
lines[#lines + 1] = 'File:' .. row[v.gallery.image] .. '|' .. ( v.gallery.caption_prefix or '' ) .. ( row[v.gallery.caption] or '' )
end
out[#out + 1] = frame:extensionTag( 'gallery', table.concat( lines, '\n' ),
{ mode = 'traditional', widths = v.gallery.width, heights = v.gallery.height } )
return frame:preprocess( table.concat( out, '\n' ) )
end
end
local header = {}
local header = {}
for _, c in ipairs( v.columns ) do
for _, c in ipairs( v.columns ) do
header[#header + 1] = c.label
header[#header + 1] = c.label
end
-- Tables with many columns start below the infobox, so they get the page's full width.
-- (Counted from header: "#" doesn't work on tables from mw.loadData.)
local wide = #header >= 7
if wide then
table.insert( out, 1, '<div class="gd-wide">' )
end
end
out[#out + 1] = '{| class="wikitable' .. ( v.group and '' or ' sortable' ) .. '"'
out[#out + 1] = '{| class="wikitable' .. ( v.group and '' or ' sortable' ) .. '"'
Line 538: Line 625:
if v.group and row[v.group] ~= current then
if v.group and row[v.group] ~= current then
current = row[v.group]
current = row[v.group]
out[#out + 1] = '|-\n! colspan="' .. #v.columns .. '" | ' .. cell( { field = v.group }, defs[v.group], current )
out[#out + 1] = '|-\n! colspan="' .. #header .. '" | ' .. cell( { field = v.group }, defs[v.group], current )
end
end
local cells = {}
local cells = {}
Line 547: Line 634:
end
end
out[#out + 1] = '|}'
out[#out + 1] = '|}'
if wide then
out[#out + 1] = '</div>'
end
return frame:preprocess( table.concat( out, '\n' ) )
return frame:preprocess( table.concat( out, '\n' ) )
end
end
Line 581: Line 671:
end
end
local args = frame:getParent().args
local args = frame:getParent().args
local out = {
local out = { '{| class="wikitable"' }
"''Game data owned by the LongJump export. Edit the game, not this page.''",
'{| class="wikitable"',
}
for _, f in ipairs( t.fields ) do
for _, f in ipairs( t.fields ) do
local value = args[f.name]
local value = args[f.name]

Latest revision as of 00:29, 4 November 2026

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

-- Renders LongJump game data stored in Cargo: infoboxes, stat lines, lists of
-- related objects, and the record table shown on Data: pages. Which fields each
-- type has, and how they display, comes from Module:GameData/schema, which the
-- generator writes from the game's types file.

local p = {}

local schema = mw.loadData( 'Module:GameData/schema' )

-- The game's keyword colors, loaded on first use.
local keywordIndex

local function isWordChar( c )
	return c ~= '' and c:match( '[%w_]' ) ~= nil
end

-- Colors keywords the way the game does (TextService.ColorizeText): one pass, longest
-- keyword first, whole words only where the keyword starts or ends with a word
-- character, written with the keyword's own capitalisation.
local function colorize( text )
	if not keywordIndex then
		keywordIndex = {}
		for _, entry in ipairs( mw.loadData( 'Module:GameData/keywords' ) ) do
			local first = entry[1]:sub( 1, 1 )
			keywordIndex[first] = keywordIndex[first] or {}
			table.insert( keywordIndex[first], entry )
		end
	end
	-- Byte-wise lower case keeps positions aligned with the original text.
	local lower = text:lower()
	local out, i = {}, 1
	while i <= #text do
		local matched
		for _, entry in ipairs( keywordIndex[lower:sub( i, i )] or {} ) do
			local key = entry[1]
			if lower:sub( i, i + #key - 1 ) == key
				and not ( isWordChar( key:sub( 1, 1 ) ) and isWordChar( text:sub( i - 1, i - 1 ) ) )
				and not ( isWordChar( key:sub( -1 ) ) and isWordChar( text:sub( i + #key, i + #key ) ) ) then
				matched = entry
				break
			end
		end
		if matched then
			out[#out + 1] = '<span class="kw" style="--kw:#' .. matched[3] .. '">' .. matched[2] .. '</span>'
			i = i + #matched[1]
		else
			out[#out + 1] = text:sub( i, i )
			i = i + 1
		end
	end
	return table.concat( out )
end

-- A name as text. Page names can hold wiki markup: two apostrophes in a row
-- ("Kunoichi''") would start italics, so they are written as character references.
-- Names without them come back unchanged.
local function nameText( name )
	return ( tostring( name ):gsub( "''", '&#39;&#39;' ) )
end

-- A name inside an infobox tag. PortableInfobox decodes character references before
-- parsing its content, so the apostrophes are kept in nowiki instead.
local function infoboxText( name )
	return ( tostring( name ):gsub( "''", "<nowiki>''</nowiki>" ) )
end

-- The in-game name of each page whose title differs from it ("Arrowstorm (skill)" is
-- named "Arrowstorm"), written by the generator. Links show the name, not the title.
local namesLoaded, names = pcall( mw.loadData, 'Module:GameData/names' )
if not namesLoaded then
	names = {}
end

-- A link to a page, labelled with its in-game name shown literally.
local function pageLink( title )
	local label = names[title] or title
	if label ~= title or tostring( label ):find( "''", 1, true ) then
		return '[[' .. title .. '|' .. nameText( label ) .. ']]'
	end
	return '[[' .. title .. ']]'
end

local function quote( value )
	return "'" .. ( tostring( value ):gsub( "'", "''" ) ) .. "'"
end

local function trim( s )
	return ( s:gsub( '^%s+', '' ):gsub( '%s+$', '' ) )
end

local function split( value )
	local items = {}
	for item in tostring( value ):gmatch( '[^,]+' ) do
		item = trim( item )
		if item ~= '' then
			items[#items + 1] = item
		end
	end
	return items
end

local function fieldNames( t )
	local names = {}
	for _, f in ipairs( t.fields ) do
		names[#names + 1] = f.name
	end
	return table.concat( names, ',' )
end

local function currentPage( frame )
	local page = frame.args.page
	if page and page ~= '' then
		return page
	end
	return mw.title.getCurrentTitle().text
end

-- Formats one stored value for display, or returns nil when there is nothing to show.
local function formatValue( f, value, row )
	if value == nil or value == '' then
		return nil
	end
	if f.type == 'Boolean' then
		-- Yes/no fields only show when true.
		return ( value == '1' or value == 1 or value == true ) and 'Yes' or nil
	end
	-- Wording with a slot for the value ("+{0}% to All Resistances") shows "#" there.
	value = ( tostring( value ):gsub( '{0}', '#' ) )
	local items = f.list and split( value ) or { tostring( value ) }
	local levels = f.levels and row and row[f.levels] and split( row[f.levels] ) or {}
	for i, item in ipairs( items ) do
		-- References to things that have no page in the wiki stay plain text.
		if f.link and mw.title.new( item ) and mw.title.new( item ).exists then
			item = pageLink( item )
		end
		if levels[i] then
			item = item .. ' (level ' .. levels[i] .. ')'
		end
		items[i] = item .. ( f.suffix or '' )
	end
	return table.concat( items, ', ' )
end

-- Every query passes orderBy: Cargo 3.7's default order names list fields with the
-- full table name while the query uses a short alias, which MariaDB rejects.
local function fetchRow( t, page )
	local rows = mw.ext.cargo.query( t.table, fieldNames( t ), {
		where = 'wiki_page=' .. quote( page ),
		orderBy = 'wiki_page',
		limit = 1,
	} )
	return rows[1]
end

-- Reads the object's Data: page so MediaWiki records it as embedded in the current
-- page: when the export rewrites the Data: page, MediaWiki re-renders this page.
-- (Cargo records that dependency only for #cargo_query, not for Lua queries.)
local function dependOnData( t, page )
	local title = mw.title.new( 'Data:' .. t.label .. '/' .. page )
	if title then
		title:getContent()
	end
end

-- {{#invoke:GameData|infobox|type=Skill}} on an article.
function p.infobox( frame )
	local t = schema.types[frame.args.type]
	if not t then
		return ''
	end
	local page = currentPage( frame )
	dependOnData( t, page )
	local row = fetchRow( t, page )
	if not row then
		return ''
	end
	local out = { '<infobox>', '<title><default>' .. infoboxText( row.name or page ) .. '</default></title>' }
	if row.icon and row.icon ~= '' then
		out[#out + 1] = '<image><default>' .. row.icon .. '</default></image>'
	end
	for _, f in ipairs( t.fields ) do
		if f.label then
			local value = formatValue( f, row[f.name], row )
			-- Plain words start with a capital, as the game shows them ("Monster").
			if value and f.type == 'String' and not f.link and not f.list then
				value = mw.ustring.upper( mw.ustring.sub( value, 1, 1 ) ) .. mw.ustring.sub( value, 2 )
			end
			if value then
				out[#out + 1] = '<data><label>' .. f.label .. '</label><default>' .. value .. '</default></data>'
			end
		end
	end
	out[#out + 1] = '</infobox>'
	return frame:preprocess( table.concat( out ) ) .. '[[Category:' .. t.category .. ']]'
end

-- {{#invoke:GameData|statlines|type=Skill}}: the game's own effect lines for this page,
-- each linking to every other source of the same stat, under an "Effects" heading.
function p.statlines( frame )
	local t = schema.types[frame.args.type]
	if not t then
		return ''
	end
	local page = currentPage( frame )
	dependOnData( t, page )
	local rows = mw.ext.cargo.query( 'StatLine', 'stat,text,value,scaling,ordinal', {
		where = 'wiki_page=' .. quote( page ),
		orderBy = 'ordinal',
	} )
	if #rows == 0 then
		return ''
	end
	local groups, order = {}, {}
	local shown = {}
	for _, row in ipairs( rows ) do
		-- A line with a slot for its value and a value of 0 does nothing: left out.
		if not ( row.text:find( '{0}', 1, true ) and tonumber( row.value ) == 0 ) then
			shown[#shown + 1] = row
		end
	end
	if #shown == 0 then
		return ''
	end
	rows = shown
	for _, row in ipairs( rows ) do
		local scaling = row.scaling or ''
		if not groups[scaling] then
			groups[scaling] = {}
			order[#order + 1] = scaling
		end
		local value = tostring( tonumber( row.value ) or row.value ):gsub( '%.0$', '' )
		local line = colorize( ( row.text:gsub( '{0}', ( value:gsub( '%%', '%%%%' ) ) ) ) )
		-- Some of the game's lines are fixed text with no slot for the value. Show the
		-- text as players see it, and the data's own value beside it unless it's a switch.
		if not row.text:find( '{0}', 1, true ) and value ~= '1' then
			line = line .. " <small>(game data value: " .. value .. ")</small>"
		end
		local url = mw.title.new( 'Special:Drilldown/StatLine' ):fullUrl( { stat = row.stat } )
		table.insert( groups[scaling], '* <span class="plainlinks">[' .. url .. ' ' .. line .. ']</span>' )
	end
	-- Like the game's own skill tooltips: one-time effects first, then the per-point ones.
	table.sort( order, function ( a, b )
		return ( a == 'per point' and 1 or 0 ) < ( b == 'per point' and 1 or 0 )
	end )
	local out = { '== Effects ==' }
	for _, scaling in ipairs( order ) do
		if scaling ~= 'flat' and scaling ~= '' then
			-- "Per point invested:" as in the game's skill tooltips; "Per pick:", "Per stack:".
			local header = scaling:gsub( '^%l', string.upper ) .. ( scaling:find( 'point' ) and ' invested' or '' )
			out[#out + 1] = "''" .. header .. ":''"
		end
		out[#out + 1] = table.concat( groups[scaling], '\n' )
	end
	return frame:preprocess( table.concat( out, '\n' ) )
end

-- {{#invoke:GameData|related|type=SkillTree}}: lists of other objects that refer
-- to this one, as declared in the schema ("Skills" on a skill tree page).
function p.related( frame )
	local t = schema.types[frame.args.type]
	if not t then
		return ''
	end
	local page = currentPage( frame )
	local out = {}
	for _, rel in ipairs( t.related or {} ) do
		local other = schema.types[rel.type]
		local field
		for _, f in ipairs( other.fields ) do
			if f.name == rel.field then
				field = f
			end
		end
		local rows
		if field.list then
			-- List values live in a helper table (<table>__<field>); HOLDS has the same
			-- alias problem as the default order, so join the helper table directly.
			local helper = other.table .. '__' .. rel.field
			rows = mw.ext.cargo.query( other.table .. ',' .. helper, other.table .. '.wiki_page=wiki_page', {
				join = other.table .. '._ID=' .. helper .. '._rowID',
				where = helper .. '._value=' .. quote( page ),
				orderBy = other.table .. '.wiki_page',
				limit = 500,
			} )
		else
			rows = mw.ext.cargo.query( other.table, 'wiki_page', {
				where = rel.field .. '=' .. quote( page ),
				orderBy = 'wiki_page',
				limit = 500,
			} )
		end
		if #rows > 0 then
			out[#out + 1] = '== ' .. rel.label .. ' =='
			for _, row in ipairs( rows ) do
				out[#out + 1] = '* ' .. pageLink( row.wiki_page )
			end
		end
	end
	-- List fields shown as their own section rather than in the infobox.
	local own
	for _, f in ipairs( t.fields ) do
		if f.section then
			own = own or fetchRow( t, page )
			local items = own and own[f.name] and own[f.name] ~= '' and split( own[f.name] ) or {}
			if #items > 0 then
				out[#out + 1] = '== ' .. f.section .. ' =='
				for _, item in ipairs( items ) do
					out[#out + 1] = '* ' .. ( f.link and pageLink( item ) or item )
				end
			end
		end
	end
	return frame:preprocess( table.concat( out, '\n' ) )
end

-- {{#invoke:GameData|text|type=Ability|field=description}}: text written in the
-- game itself, quoted as players see it.
function p.text( frame )
	local t = schema.types[frame.args.type]
	if not t then
		return ''
	end
	local page = currentPage( frame )
	dependOnData( t, page )
	local row = fetchRow( t, page )
	local value = row and row[frame.args.field or 'description']
	if not value or value == '' then
		return ''
	end
	local label = frame.args.label or t.text_label or 'In-game text'
	return frame:preprocess( '<blockquote class="game-text">' .. colorize( value ) .. "<br><small>''" .. label .. "''</small></blockquote>" )
end

-- {{#invoke:GameData|intro|type=Skill}}: the type's intro sentence from the schema,
-- with {field} placeholders filled from the current data.
function p.intro( frame )
	local t = schema.types[frame.args.type]
	if not t then
		return ''
	end
	local page = currentPage( frame )
	dependOnData( t, page )
	local row = fetchRow( t, page )
	if not row or not t.intro then
		return ''
	end
	local fields = {}
	for _, f in ipairs( t.fields ) do
		fields[f.name] = f
	end
	local text = t.intro:gsub( '{([%w_]+)}', function ( name )
		local value = fields[name] and formatValue( fields[name], row[name], row )
		if value and name == 'name' then
			value = nameText( value )
		end
		return value and ( value:gsub( '%%', '%%%%' ) ) or ''
	end )
	return frame:preprocess( text )
end

-- Gallery images fit a square box of this many pixels, scaled by a whole number (up by
-- the largest factor that fits, or down by the smallest divisor that fits), so pixel art
-- keeps square pixels.
local GALLERY_BOX = 96

local function galleryImage( file, page )
	local title = mw.title.new( 'File:' .. file )
	-- Reading a file's size is an "expensive" call, limited per page; past the limit the
	-- image is fitted to the box by the browser instead.
	local ok, info = pcall( function () return title and title.file end )
	local width, height = ok and info and info.width, ok and info and info.height
	if not width or width == 0 or height == 0 then
		return '[[File:' .. file .. '|' .. GALLERY_BOX .. 'x' .. GALLERY_BOX .. 'px|link=' .. page .. ']]'
	end
	local side = math.max( width, height )
	if side <= GALLERY_BOX then
		local factor = math.floor( GALLERY_BOX / side )
		width, height = width * factor, height * factor
	else
		local divisor = math.ceil( side / GALLERY_BOX )
		width, height = math.floor( width / divisor ), math.floor( height / divisor )
	end
	return '<span class="gd-gallery-fit" style="width:' .. width .. 'px;height:' .. height .. 'px">[[File:'
		.. file .. '|link=' .. page .. ']]</span>'
end

-- {{#invoke:GameData|gallery|type=Class}}: every object of a type as a grid of icons.
function p.gallery( frame )
	local t = schema.types[frame.args.type]
	if not t then
		return ''
	end
	local rows = mw.ext.cargo.query( t.table, 'wiki_page,icon', { orderBy = 'wiki_page', limit = 500 } )
	local out = { '<div class="gd-gallery">' }
	for _, row in ipairs( rows ) do
		local image = ''
		if row.icon and row.icon ~= '' then
			image = '<div class="gd-gallery-image">' .. galleryImage( row.icon, row.wiki_page ) .. '</div>'
		end
		out[#out + 1] = '<div class="gd-gallery-item">' .. image .. pageLink( row.wiki_page ) .. '</div>'
	end
	out[#out + 1] = '</div>'
	return frame:preprocess( table.concat( out, '\n' ) )
end

local function rowsFor( tableName, fields, page )
	return mw.ext.cargo.query( tableName, fields, {
		where = 'wiki_page=' .. quote( page ),
		orderBy = 'ordinal',
		limit = 500,
	} )
end

-- Puts a value range into a modifier's wording: "+{0}% to All Resistances" with "4-6"
-- gives "+(4-6)% to All Resistances".
local function withValue( text, value )
	value = tostring( value or '' )
	local shown = value:find( '-', 2, true ) and ( '(' .. value .. ')' ) or value
	return ( text:gsub( '{0}', ( shown:gsub( '%%', '%%%%' ) ) ) )
end

-- {{Modifier tiers|type=Modifier}}: the modifier's tiers for each slot or crystal color,
-- as the in-game Mod Dictionary shows them (T1 is the best).
function p.tiers( frame )
	local t = schema.types[frame.args.type]
	if not t then
		return ''
	end
	local page = currentPage( frame )
	dependOnData( t, page )
	local row = fetchRow( t, page )
	local tiers = rowsFor( 'ModifierTier', 'applies_to,tier,value,level,weight', page )
	if not row or #tiers == 0 then
		return ''
	end
	local out = { '== Tiers ==', '{| class="wikitable gd-tiers"', '! Tier !! Effect !! Required item level' }
	local current
	for _, tier in ipairs( tiers ) do
		if tier.applies_to ~= current then
			current = tier.applies_to
			out[#out + 1] = '|-\n! colspan="3" | ' .. current .. ' (weight ' .. ( tier.weight or '' ) .. ')'
		end
		out[#out + 1] = '|-\n| T' .. tier.tier .. ' || ' .. colorize( withValue( row.text or '', tier.value ) ) .. ' || ' .. ( tier.level or '' )
	end
	out[#out + 1] = '|}'
	return frame:preprocess( table.concat( out, '\n' ) )
end

-- {{Unique modifiers|type=UniqueItem}}: a unique's fixed modifiers, with their value
-- range on each difficulty.
function p.uniquemods( frame )
	local t = schema.types[frame.args.type]
	if not t then
		return ''
	end
	local page = currentPage( frame )
	dependOnData( t, page )
	local mods = rowsFor( 'UniqueModifier', 'text,implicit,mild,spicy,inferno,nightmare', page )
	if #mods == 0 then
		return ''
	end
	local out = { '== Modifiers ==', '{| class="wikitable gd-unique-mods"', '! Modifier !! Mild !! Spicy !! Inferno !! Nightmare' }
	for _, mod in ipairs( mods ) do
		-- The wording's slot shows the whole range across the difficulty columns.
		local low, high
		for _, value in ipairs( { mod.mild, mod.spicy, mod.inferno, mod.nightmare } ) do
			for number in tostring( value or '' ):gmatch( '%-?%d+%.?%d*' ) do
				number = tonumber( number )
				if number then
					low = low and math.min( low, number ) or number
					high = high and math.max( high, number ) or number
				end
			end
		end
		local range = low and ( low == high and tostring( low ) or ( low .. '-' .. high ) ) or '#'
		local text = colorize( withValue( mod.text or '', range ) )
		if mod.implicit == '1' then
			text = text .. " <small>''(implicit)''</small>"
		end
		out[#out + 1] = '|-\n| ' .. text .. ' || ' .. ( mod.mild or '' ) .. ' || ' .. ( mod.spicy or '' )
			.. ' || ' .. ( mod.inferno or '' ) .. ' || ' .. ( mod.nightmare or '' )
	end
	out[#out + 1] = '|}'
	return frame:preprocess( table.concat( out, '\n' ) )
end

-- {{Skill item modifiers|type=Skill}}: the item modifiers that add levels to this skill.
function p.skillmods( frame )
	local t = schema.types[frame.args.type]
	if not t then
		return ''
	end
	local page = currentPage( frame )
	dependOnData( t, page )
	local mods = rowsFor( 'SkillItemModifier', 'slot,affix,tiers,weight', page )
	if #mods == 0 then
		return ''
	end
	local out = { '== Rolls on items ==', '{| class="wikitable"', '! Slot !! Affix !! Skill levels by tier !! Weight' }
	for _, mod in ipairs( mods ) do
		out[#out + 1] = '|-\n| ' .. ( mod.slot or '' ) .. ' || ' .. ( mod.affix or '' ) .. ' || ' .. ( mod.tiers or '' ) .. ' || ' .. ( mod.weight or '' )
	end
	out[#out + 1] = '|}'
	return frame:preprocess( table.concat( out, '\n' ) )
end

-- {{Craft offers|type=Craft}}: every place that offers a craft, with its base costs.
function p.craftoffers( frame )
	local t = schema.types[frame.args.type]
	if not t then
		return ''
	end
	local page = currentPage( frame )
	dependOnData( t, page )
	local offers = rowsFor( 'CraftOffer',
		'offered_by,location,description,gold,essence,crafting_tokens,ultimate_tokens,chaos_added,success_rate,rare_offer', page )
	if #offers == 0 then
		return ''
	end
	local out = { '== Where to craft it ==',
		"''Base costs: the game raises them with the item's Chaos and level.''",
		'{| class="wikitable"', '! Offered by !! Where !! Cost !! Chaos added !! Success rate !! Effect' }
	for _, offer in ipairs( offers ) do
		local cost = {}
		for _, part in ipairs( { { 'gold', 'Gold' }, { 'essence', 'Nightmare Essence' },
			{ 'crafting_tokens', 'Crafting Tokens' }, { 'ultimate_tokens', 'Ultimate Tokens' } } ) do
			if offer[part[1]] and offer[part[1]] ~= '' and offer[part[1]] ~= '0' then
				cost[#cost + 1] = offer[part[1]] .. ' ' .. part[2]
			end
		end
		local who = offer.offered_by .. ( offer.rare_offer == '1' and " <small>''(rare offer)''</small>" or '' )
		out[#out + 1] = '|-\n| ' .. who .. ' || ' .. ( offer.location or '' ) .. ' || ' .. ( #cost > 0 and table.concat( cost, ', ' ) or 'Free' )
			.. ' || ' .. ( offer.chaos_added or '0' ) .. ' || ' .. ( offer.success_rate or '' ) .. '% || ' .. colorize( offer.description or '' )
	end
	out[#out + 1] = '|}'
	return frame:preprocess( table.concat( out, '\n' ) )
end

-- One cell of a table view: links to pages that exist, game keyword colors on request.
local function cell( column, def, value )
	if value == nil or value == '' then
		return ''
	end
	if def and def.type == 'Boolean' then
		return ( value == '1' or value == 1 ) and 'Yes' or ''
	end
	value = tostring( value )
	if column.colorize then
		value = colorize( value )
	end
	local link = column.link or ( def and def.link )
	if ( link or column.field == 'wiki_page' ) and mw.title.new( value ) and mw.title.new( value ).exists then
		value = pageLink( value )
	end
	return value .. ( column.suffix or '' )
end

-- {{Data table|type=Monster|view=MonsterSpawn}}: a row table as declared in the type's
-- blocks. A view shows the page's own rows, or with "match" the rows of other pages
-- whose given field names this page. "group" starts a header row whenever its value changes.
function p.datatable( frame )
	local t = schema.types[frame.args.type]
	local v = t and t.views and t.views[frame.args.view]
	if not v then
		return ''
	end
	local page = currentPage( frame )
	dependOnData( t, page )
	local defs = {}
	for _, f in ipairs( schema.tables[v.table] or {} ) do
		defs[f.name] = f
	end
	local names, seen = {}, {}
	local function want( name )
		if name and not seen[name] then
			seen[name] = true
			names[#names + 1] = name
		end
	end
	for _, c in ipairs( v.columns ) do
		want( c.field )
	end
	want( v.group )
	if v.gallery then
		want( v.gallery.image )
		want( v.gallery.caption )
	end
	local match = v.match or 'wiki_page'
	local rows = mw.ext.cargo.query( v.table, table.concat( names, ',' ), {
		where = match .. '=' .. quote( page ),
		orderBy = v.order or ( match == 'wiki_page' and 'ordinal' or 'wiki_page,ordinal' ),
		limit = 5000,
	} )
	if #rows == 0 then
		return ''
	end
	local out = { '== ' .. v.heading .. ' ==' }
	if v.note then
		out[#out + 1] = "''" .. v.note .. "''"
	end
	-- A view with "gallery" shows its rows as an image gallery: the file and caption fields.
	if v.gallery then
		local lines = {}
		for _, row in ipairs( rows ) do
			lines[#lines + 1] = 'File:' .. row[v.gallery.image] .. '|' .. ( v.gallery.caption_prefix or '' ) .. ( row[v.gallery.caption] or '' )
		end
		out[#out + 1] = frame:extensionTag( 'gallery', table.concat( lines, '\n' ),
			{ mode = 'traditional', widths = v.gallery.width, heights = v.gallery.height } )
		return frame:preprocess( table.concat( out, '\n' ) )
	end
	local header = {}
	for _, c in ipairs( v.columns ) do
		header[#header + 1] = c.label
	end
	-- Tables with many columns start below the infobox, so they get the page's full width.
	-- (Counted from header: "#" doesn't work on tables from mw.loadData.)
	local wide = #header >= 7
	if wide then
		table.insert( out, 1, '<div class="gd-wide">' )
	end
	out[#out + 1] = '{| class="wikitable' .. ( v.group and '' or ' sortable' ) .. '"'
	out[#out + 1] = '! ' .. table.concat( header, ' !! ' )
	local current
	for _, row in ipairs( rows ) do
		if v.group and row[v.group] ~= current then
			current = row[v.group]
			out[#out + 1] = '|-\n! colspan="' .. #header .. '" | ' .. cell( { field = v.group }, defs[v.group], current )
		end
		local cells = {}
		for _, c in ipairs( v.columns ) do
			cells[#cells + 1] = cell( c, defs[c.field], row[c.field] )
		end
		out[#out + 1] = '|-\n| ' .. table.concat( cells, ' || ' )
	end
	out[#out + 1] = '|}'
	if wide then
		out[#out + 1] = '</div>'
	end
	return frame:preprocess( table.concat( out, '\n' ) )
end

-- {{#invoke:GameData|skillmods_all}}: every "+N to <skill>" item modifier, by slot.
function p.skillmods_all( frame )
	local mods = mw.ext.cargo.query( 'SkillItemModifier', 'wiki_page,slot,affix,tiers', {
		orderBy = 'slot,wiki_page',
		limit = 5000,
	} )
	local out, current = {}, nil
	for _, mod in ipairs( mods ) do
		if mod.slot ~= current then
			if current then
				out[#out + 1] = '|}'
			end
			current = mod.slot
			out[#out + 1] = '== ' .. current .. ' =='
			out[#out + 1] = '{| class="wikitable sortable"\n! Skill !! Affix !! Skill levels by tier'
		end
		out[#out + 1] = '|-\n| ' .. pageLink( mod.wiki_page ) .. ' || ' .. ( mod.affix or '' ) .. ' || ' .. ( mod.tiers or '' )
	end
	if current then
		out[#out + 1] = '|}'
	end
	return frame:preprocess( table.concat( out, '\n' ) )
end

-- Used by the "<Label> data" templates: shows the stored record on its Data: page.
function p.record( frame )
	local t = schema.types[frame.args.type]
	if not t then
		return ''
	end
	local args = frame:getParent().args
	local out = { '{| class="wikitable"' }
	for _, f in ipairs( t.fields ) do
		local value = args[f.name]
		if value and value ~= '' then
			out[#out + 1] = '|-\n! ' .. f.name .. '\n| ' .. ( formatValue( f, value, args ) or '' )
		end
	end
	out[#out + 1] = '|}'
	if args.wiki_page and args.wiki_page ~= '' then
		out[#out + 1] = 'Article: ' .. pageLink( args.wiki_page )
	end
	return frame:preprocess( table.concat( out, '\n' ) )
end

return p