Terraria Wiki
Advertisement
Terraria Wiki
Die druckbare Version wird nicht mehr unterstützt und kann Darstellungsfehler aufweisen. Bitte aktualisiere deine Browser-Lesezeichen und verwende stattdessen die Standard-Druckfunktion des Browsers.
Siehe auch die englische Modulseite: Module:Recipes. Sie enthält möglicherweise umfassendere oder aktuellere Informationen.

Für dieses Modul gibt es noch keine Dokumentations-Unterseite. Erstelle jetzt eine.


------- l10n info --------------
local l10n_info = mw.loadData('Module:Recipes/l10n')
local lang -- cache current lang.
local l10n_table

------- The following is not related to l10n. --------------

local item_link = require('Module:Item').go
local trim = mw.text.trim
local cargo = mw.ext.cargo
local tag = mw.text.tag
local cache = require 'mw.ext.LuaCache'

local currentFrame -- global cache for current frame object.
local inputArgs -- global args cache.
--local resultanchor

-- The order in which all columns are displayed is:
-- col-A • Result • col-B • Ingredients • col-C • station-col-before • Crafting Station • station-col-after • Col-D 
local extCols_A = 0
local extCols_B = 0
local extCols_C = 0
local extCols_D = 0
local extCols_stationBefore = 0
local extCols_stationAfter = 0


local options = {
	--resultanchor = nil,
	result_order = 'result',
	needCate = 1,
	linkResult = true,
	showResultId = false,
	needGroup = true,
	--withStation = true,
	--stationGroup = withStation,
	--resultTemplate = nil
}

local initOptions = function(args)
	local _resultanchor = getArg('resultanchor') or 'y' -- the default is 'y'
	if _resultanchor == 'n' or _resultanchor == 'no' then
		options.resultanchor = nil
	else
		options.resultanchor = _resultanchor
	end

	local _result_order = getArg('orderbyid')
	if _result_order == 'y' or _result_order == 'yes' then
		options.result_order = 'resultid'
	end

	local _cate = getArg('cate')
	if _cate == 'force' or _cate == 'all' then
		options.needCate = 2
	elseif _cate == 'n' or _cate == 'no' then
		options.needCate = nil
	end

	local _link = getArg('link')
	if _link == 'n' or _link == 'no' then
		options.linkResult = false
	end

	if getArg('showresultid') then
		options.showResultId = true
	end

	options.withStation = not getArg('nostation')
	options.stationGroup = options.withStation
	if options.withStation then
		local grouping = getArg('stationgrouping')
		if grouping == 'n' or grouping == 'no' then
			options.stationGroup = false
		end
		
	end

	local _needGroup = getArg('grouping')
	if _needGroup == 'n' or _needGroup == 'no' then
		options.needGroup = false
	end

	options.resultTemplate = getArg('resulttemplate')
end

-- credit: http://richard.warburton.it
-- this version is with trim.
local explode = function(div,str)
	if (div=='') then return false end
	local pos,arr = 0,{}
	-- for each divider found
	for st,sp in function() return string.find(str,div,pos,true) end do
		table.insert(arr, trim(string.sub(str,pos,st-1))) -- Attach chars left of current divider
		pos = sp + 1 -- Jump past current divider
	end
	table.insert(arr, trim(string.sub(str,pos))) -- Attach chars right of last divider
	return arr
end

local l10n = function(key)
	return l10n_table[key] or l10n_info['en'][key]
end

function getArg(key)
	local v = trim(inputArgs[key] or '')
	if v=='' then
		return nil
	else
		return v
	end
end

local tr = (function()
	local cache = {}
	return function(text, lang, link)
		local key = lang..'|'..text
		if link then
			key = key .. '|l'
		end
		local str = cache[key]
		if not str then
			str = currentFrame:expandTemplate{ title = 'tr', args = {text, lang=lang, link=link}}
			cache[key] = str
		end
		return str
	end
end)()

local itemLink = (function()
	local cache = {}
	return function(name, args)
		local key = name.."|"
		if args then
			for k, v in pairs(args) do
				key = key..k..'='..tostring(v)..'|'
			end
		end
		if not cache[key] then
			local args = args and mw.clone(args) or {}
			local pos = string.find(name, '#', 1, true)
			if pos then
				-- extract ext args from name
				-- <itemname>(#i:<image>)?(#n:<note>)?
				local item = string.sub(name, 1, pos-1)
				for k, v in string.gmatch(name, "#(%l):([^#]+)") do
					if k == 'i' then
						-- #i:old is a shortcut for #i:& (old).png
						if(v == 'old') then
							args['image'] = item .. ' (old).png'
						else
							args['image'] = string.gsub(v, '&', item)
						end
					elseif k == 'n' then
						args['note2'] = v
					end
				end
				name = item
			end
			args[1] = name
			if (not args[2]) or args[2]=='' then
				args[2] = tr(name, lang)
			end
			args['small'] = 'y'
			args['lang'] = lang or 'en'
			args['nolink'] = args['nolink'] and 'y' or nil
--			args['anchor'] = args['anchor'] and 'y' or nil
			cache[key] = item_link(currentFrame, args)
		end
		return cache[key]
	end
end)()

local getVersionIconsStr = (function()
	local local_cache = {}
	return function(version)
		local key = lang..':recipes:versionicon:'..version
		-- {{version icons}} is a slow template, so cache its result:
		local vstr = local_cache[key] or cache.get(key) -- cache for current lang
		if not vstr then
			vstr = currentFrame:expandTemplate{ title = 'version icons', args = {version} }
			cache.set(key, vstr, 3600*24) -- cache 24hr.
			local_cache[key] = vstr
		end
		return vstr
	end
end)()

-- for xxx/yyy, return an array of itemname, split xxx/yyy to item1=xxx, item2=yyy.
-- If it's something like "Lead/Iron Bar", it will normalize as item1 = Iron Bar, item2 = Lead Bar.
-- for single name, return nil.
local split = (function()
	local metals = {
		['Copper/Tin'] = 1,
		['Iron/Lead'] = 1,
		['Silver/Tungsten'] = 1,
		['Gold/Platinum'] = 1,
		['Cobalt/Palladium'] = 1,
		['Mythril/Orichalcum'] = 1,
		['Adamantite/Titanium'] = 1,
		-- the same ones as above but in reversed order
		['Tin/Copper'] = 2,
		['Lead/Iron'] = 2,
		['Tungsten/Silver'] = 2,
		['Platinum/Gold'] = 2,
		['Palladium/Cobalt'] = 2,
		['Orichalcum/Mythril'] = 2,
		['Titanium/Adamantite'] = 2,
	}
	return function(name)
		local count = select(2, name:gsub("/", "/", 2))
		if count == 0 then
			-- only 1 item
			return nil
		elseif count == 1 then
			-- 2 items
			local item1a, item1b, item2a, item2b = name:match("^%s*(%S+)%s*(.-)/%s*(%S+)%s*(.-)$")
			local x = metals[item1a..'/'..item2a]
			if tostring(item1b) == '' and x then
				item1b = item2b
			end
			if x == 2 then
				return {trim(item2a..' '..item2b), trim(item1a..' '..item1b)}
			else
				return {trim(item1a..' '..item1b), trim(item2a..' '..item2b)}
			end
		else
			-- 3 or more items
			return explode('/', name)
		end
	end
end)()

-- normalize ingredient name input:
-- * Lead Bar=>¦Lead Bar¦, Iron/Lead Bar => ¦Iron Bar¦Lead Bar¦, Lead/Iron Bar => ¦Iron Bar¦Lead Bar¦
-- * strip #i: and #n: 
local normalize = function(name)
	local list = split(name)
	local strip = function(str)
		return string.gsub(str, '#.+', '')
	end
	if not list then
		return  '¦' .. strip(name) .. '¦'
	end
	local result = {''}
	for _, v in ipairs(list) do
		result[#result+1] = strip(v)
	end
	result[#result+1] = ''
	return table.concat(result, '¦')
end

local escape = function(str)
	return str:gsub("'", "\\'"):gsub("&#39;", "\\'")
end
local enclose = function(str)
	return "'" .. escape(str) .. "'"
end

local getItemGroupName = (function()
	local groupIndex = {
		[1] = 'Any Wood',
		[2] = 'Any Iron Bar',
		[3] = 'Any Sand',
		[4] = 'Any Pressure Plate',
		[5] = 'Any Bird',
		[6] = 'Any Scorpion',
		[7] = 'Any Squirrel',
		[8] = 'Any Jungle Bug',
		[9] = 'Any Duck',
		[10] = 'Any Butterfly',
		[11] = 'Any Firefly',
		[12] = 'Any Snail',
		[13] = 'Any Fruit',
		[14] = 'Any Dragonfly',
		[15] = 'Any Turtle',
		[16] = 'Any Macaw',
		[17] = 'Any Cockatiel',
		[18] = 'Any Cloud Balloon',
		[19] = 'Any Blizzard Balloon',
		[20] = 'Any Sandstorm Balloon',
		[21] = 'Any Balloon',
		[22] = 'Any Guide to Critter Companionship',
		[23] = 'Any Guide to Environmental Preservation'
	}
	local groupIndexInfo = {
		['Wood'] = 1,  ['Ebonwood'] = 1, ['Rich Mahogany'] = 1, ['Pearlwood'] = 1, ['Shadewood'] = 1,
		['Spooky Wood'] = 1, ['Boreal Wood'] = 1, ['Palm Wood'] = 1, ['Ash Wood'] = 1,
		['Iron Bar'] = 2, ['Lead Bar'] = 2,
		['Sand Block']= 3, ['Pearlsand Block'] = 3, ['Crimsand Block'] = 3, ['Ebonsand Block'] = 3,
		['Hardened Sand Block'] = 3, ['Hardened Ebonsand Block'] = 3, ['Hardened Crimsand Block'] = 3, ['Hardened Pearlsand Block'] = 3,
		['Red Pressure Plate'] = 4, ['Green Pressure Plate'] = 4, ['Gray Pressure Plate'] = 4, ['Brown Pressure Plate'] = 4,
		['Blue Pressure Plate'] = 4, ['Yellow Pressure Plate'] = 4, ['Lihzahrd Pressure Plate'] = 4,
		['Bird'] = 5, ['Blue Jay'] = 5, ['Cardinal'] = 5,
		['Black Scorpion'] = 6,['Scorpion'] = 6,
		['Squirrel'] = 7,['Red Squirrel'] = 7,
		['Grubby'] = 8,['Sluggy'] = 8,['Buggy'] = 8,
		['Mallard Duck'] = 9, ['Duck'] = 9,
		['Sulphur Butterfly'] = 10, ['Julia Butterfly'] = 10, ['Monarch Butterfly'] = 10, ['Purple Emperor Butterfly'] = 10,
		['Red Admiral Butterfly'] = 10, ['Tree Nymph Butterfly'] = 10, ['Ulysses Butterfly'] = 10, ['Zebra Swallowtail Butterfly'] = 10,
		['Firefly'] = 11, ['Lightning Bug'] = 11,
		['Snail'] = 12,['Glowing Snail'] = 12,
		['Apple'] = 13, ['Apricot'] = 13, ['Banana'] = 13, ['Blackcurrant'] = 13, ['Blood Orange'] = 13, ['Cherry'] = 13, ['Coconut'] = 13,
		['Dragon Fruit'] = 13, ['Elderberry'] = 13, ['Grapefruit'] = 13, ['Lemon'] = 13, ['Mango'] = 13, ['Peach'] = 13, ['Pineapple'] = 13,
		['Plum'] = 13, ['Rambutan'] = 13, ['Star Fruit'] = 13, ['Spicy Pepper'] = 13, ['Pomegranate'] = 13,
		['Black Dragonfly'] = 14, ['Blue Dragonfly'] = 14, ['Green Dragonfly'] = 14, ['Orange Dragonfly']= 14,
		['Red Dragonfly'] = 14, ['Yellow Dragonfly'] = 14,
		['Turtle'] = 15, ['Jungle Turtle'] = 15,
		['Scarlet Macaw'] = 16, ['Blue Macaw'] = 16,
		['Yellow Cockatiel'] = 17, ['Gray Cockatiel'] = 17,
		['Cloud in a Balloon'] = 18, ['Blue Horseshoe Balloon'] = 18,
		['Blizzard in a Balloon'] = 19, ['White Horseshoe Balloon'] = 19,
		['Sandstorm in a Balloon'] = 20, ['Yellow Horseshoe Balloon'] = 20,
		['Silly Green Balloon'] = 21, ['Silly Pink Balloon'] = 21, ['Silly Purple Balloon'] = 21,
		['Guide to Critter Companionship'] = 22,['Guide to Critter Companionship (Inactive)'] = 22,
		['Guide to Environmental Preservation'] = 23,['Guide to Environmental Preservation (Inactive)'] = 23,
	}
	return function(item)
		return groupIndexInfo[item] and groupIndex[groupIndexInfo[item]]
	end
end)()

local normalizeStation = function(station)
	if station == 'Altar' then
		station = 'Demon Altar'
	end
	return station
end

local normalizeVersion = function(_version)
	if not _version or _version == '' then 
		return ''
	end
	_version = trim(_version):lower()
	local version = ''

	-- First check Japan; if returns true, skip all the others, same as we do with the other exclusivity templates
	if _version:find('japan', 1, true) then
		version = version .. ' japan'
	elseif _version:find('newchinese', 1, true) then
		version = version .. ' newchinese'
	else
		if _version:find('desktop', 1, true) or _version:find('pc', 1, true) then
			version = version .. ' pc'
		end
		if _version:find('console', 1, true) then
			version = version .. ' console'
		end
		if _version:find('mobile', 1, true) then
			version = version .. ' mobile'
		end
		if _version:find('old-gen', 1, true) then
			version = version .. ' old-gen'
		end
		if _version:find('windowsphone', 1, true) then
			version = version .. ' windowsphone'
		end
		if _version:find('3ds', 1, true) then
			version = version .. ' 3ds'
		end
		if _version:find('tmodloader', 1, true) then
			version = version .. ' tmodloader'
		end
		if _version:find('tmodloaderlegacy', 1, true) then
			version = version .. ' tmodloaderlegacy'
		end

		-- If every version returns true, don't display any icons
		if version == ' pc console mobile old-gen windowsphone 3ds tmodloader tmodloaderlegacy' or version == '' then
			return ''
		end
	end

	return trim(version)
end

local criStr = function(args)
	local constraints = {}
	-- station = ? and station != ?
	local _station = trim(args['station'] or '')
	local _stationnot = trim(args['stationnot'] or '')
	if _station ~= '' then
		local pieces = explode('/', _station)
		for i, v in ipairs(pieces) do
			pieces[i] = "station = " .. enclose(normalizeStation(v))
		end
		constraints[#constraints+1] = table.concat(pieces, ' OR ')
	end
	if _stationnot ~= '' then
		local pieces = explode('/', _stationnot)
		for i, v in ipairs(pieces) do
			pieces[i] = 'station <> ' .. enclose(normalizeStation(v))
		end
		constraints[#constraints+1] = table.concat(pieces, ' AND ')
	end
	-- result = ? and result != ?
	local _result = trim(args['result'] or '')
	local _resultnot = trim(args['resultnot'] or '')
	if _result ~= '' then
		local pieces = explode('/', _result)
		for i, v in ipairs(pieces) do
			if mw.ustring.sub(v, 1, 5) == 'LIKE ' then
				pieces[i] = 'result LIKE ' .. enclose(trim(mw.ustring.sub(v, 6)))
			else
				pieces[i] = 'result=' .. enclose(v)
			end
		end
		constraints[#constraints+1] = table.concat(pieces, ' OR ')
	end
	if _resultnot ~= '' then
		local pieces = explode('/', _resultnot)
		for i, v in ipairs(pieces) do
			if mw.ustring.sub(v, 1, 5) == 'LIKE ' then
				pieces[i] = 'result NOT LIKE ' .. enclose(trim(mw.ustring.sub(v, 6)))
			else
				pieces[i] = 'result <> ' .. enclose(v)
			end
		end
		constraints[#constraints+1] = table.concat(pieces, ' AND ')
	end
	-- ingredient = ?
	local _ingredient = trim(args['ingredient'] or '')
	if _ingredient ~= '' then
		local pieces = explode('/', _ingredient)
		for i, v in ipairs(pieces) do
			if mw.ustring.sub(v, 1, 1) == '#' then
				pieces[i] = "ingredients HOLDS LIKE '%¦" .. escape(mw.ustring.sub(v, 2)) .. "¦%'"
			elseif mw.ustring.sub(v, 1, 5) == 'LIKE ' then
				pieces[i] = "ingredients HOLDS LIKE '%¦" .. escape(trim(mw.ustring.sub(v, 6))) .. "¦%'"
			else
				pieces[i] = "ingredients HOLDS LIKE '%¦" .. escape(v) .. "¦%'"
				-- any xxx
				local group = getItemGroupName(v)
				if group then
					pieces[i] =  pieces[i] .. " OR ingredients HOLDS LIKE '%¦" .. escape(group) .. "¦%'"
				end
			end
		end
		constraints[#constraints+1] = table.concat(pieces, ' OR ')
	end
	--versions
	local _version = normalizeVersion(args['version'] or args['versions'] or '')
	if _version ~= '' then
		constraints[#constraints+1] = 'version = ' .. enclose(_version)
	end

	local where = ''
	if constraints[1] then
		where = '('.. table.concat(constraints, ') AND (') .. ')'
	end
	return where
end

local resultCell = function(row)
	local result, resultid, resultimage, resultnote, amount, version = row.result, row.resultid, row.resultimage, (row.resulttext or ''), row.amount, (row.version or '')
	local str
	-- args for result {{item}}
	local args = {anchor = options.resultanchor, nolink = not options.linkResult, class='multi-line'}
	--version text
	if version ~= '' then
		str = {'<div class="version-note note-text small">', l10n('version_note_before'), getVersionIconsStr(version), l10n('version_note_after'), '</div>'}
		args['icons'] = 'no' --If there is version-exclusive info, don't show ecions for result {{item}}.
	else
		str = {}
	end
	if options.showResultId then
		args['id'] = resultid
	end
	if resultimage then
		args['image'] = resultimage
	end
	str[#str+1] = itemLink(result, args)
	if amount ~= '1' then
		str[#str+1] = tag('span', {class='am'}, amount)
	end
	-- note text
	if resultnote ~= '' then
		local note_l10n = l10n('result_note') or {}
		resultnote = note_l10n[resultnote] or resultnote
		str[#str+1] = tag('div', {class="result-note note-text small"}, resultnote)
	end
	str = table.concat(str)
	if options.resultTemplate then
		local template_str = currentFrame:expandTemplate{ title = options.resultTemplate, args = {
				link = options.linkResult, showid = options.showResultId, 
				resultid=resultid, resultimage=resultimage, resultnote=resultnote,
				result=result, amount=amount, versions=version,
		} }
		str = template_str:gsub('@@@@', str)
	end
	return str
end

local ingredientsCell = function(ingredientsArgs, itemLinkArgs)
	local str = ''
	local rows = explode('^', ingredientsArgs)
	for i, v in ipairs(rows) do
		local item, amount = v:match('^(.-)¦(.-)$')
		local items = split(item)
		if not items then
			items = itemLink(item, itemLinkArgs)
		else
			for j, itemname in ipairs(items) do
				items[j] = itemLink(itemname, itemLinkArgs)
			end
			items = table.concat(items, l10n('ingredients_sep'))
		end
		if amount ~= '1' then
			items = items .. tag('span', {class="am"}, amount)
		end
		rows[i] = tag('li', nil, items)
	end
	return tag('ul', nil, table.concat(rows))
end

local stationLink = function(station, options, andStr, orStr, orStrInner)
	local il = function(s, o)
		if o then
			for k,v in pairs(o) do options[k] = v end
		end
		return itemLink(s, options)
	end
	local ectoMist = function() return il('Ecto Mist', {mode = 'text'}) end
	local water = function() return '<span class="ib">' .. il('Water') .. orStrInner .. il('Sink') .. '</span>' end
	if station == 'By Hand' then
		return l10n('station_by_hand')
	elseif station == 'Ancient Manipulator' or station == 'Autohammer'
		or station == 'Blend-O-Matic' or station == 'Bone Welder' or station == 'Bookcase'
		or station == 'Campfire' or station == 'Crystal Ball'
		or station == 'Decay Chamber' or station == 'Dye Vat'
		or station == 'Flesh Cloning Vat' or station == 'Furnace'
		or station == 'Glass Kiln'
		or station == 'Heavy Work Bench' or station == 'Hellforge' or station == 'Honey' or station == 'Honey Dispenser'
		or station == 'Ice Machine' or station == 'Imbuing Station'
		or station == 'Keg'
		or station == 'Lava' or station == 'Living Loom' or station == 'Lihzahrd Furnace' or station == 'Living Wood' or station == 'Loom'
		or station == 'Meat Grinder'
		or station == 'Sawmill' or station == 'Sky Mill' or station == 'Solidifier' or station == 'Steampunk Boiler'
		or station == 'Teapot' or station == "Tinkerer's Workshop" or station == 'Work Bench' then
			return il(station)
	-- altars
	elseif station == 'Demon Altar' then
		return il('Demon Altar') .. orStr .. il('Crimson Altar')
	-- anvils
	elseif station == 'Iron Anvil' then
		return il('Iron Anvil') .. orStr .. il('Lead Anvil')
	elseif station == 'Mythril Anvil' then
		return il('Mythril Anvil') .. orStr .. il('Orichalcum Anvil')
	-- forges
	elseif station == 'Adamantite Forge' then
		return il('Adamantite Forge') .. orStr .. il('Titanium Forge')
	-- the rest should be more or less alphabetized
	elseif station == 'Cooking Pot' then
		return il('Cooking Pot') .. orStr .. il('Cauldron')
	elseif station == 'Placed Bottle' then
		return il('Placed Bottle') .. orStr .. il('Alchemy Table')
	elseif station == 'Placed Bottle only' then
		return il('Placed Bottle')
	elseif station == 'Shimmer' then
		return il('Shimmer', {[2] = l10n('station_shimmer')})
	elseif station == 'Chlorophyte Extractinator' then
		return il('Chlorophyte Extractinator', {[2] = l10n('station_chlorophyte_extractinator')})
	elseif station == 'Water' then
		return il('Water') .. orStr .. il('Sink')
	-- combos
	elseif station == 'Crystal Ball and Lava' then
		return il('Crystal Ball') .. andStr .. il('Lava')
	elseif station == 'Crystal Ball and Honey' then
		return il('Crystal Ball') .. andStr .. il('Honey')
	elseif station == 'Crystal Ball and Water' then
		return il('Crystal Ball') .. andStr.. water()
	elseif station == 'Sky Mill and Water' then
		return il('Sky Mill') .. andStr.. water()
	elseif station == 'Sky Mill and Snow Biome' or station == 'Sky Mill and Snow biome' then
		return il('Sky Mill') .. andStr.. il('Snow biome', {mode = 'text'})
	elseif station == 'Table and Chair' then
		return il('Table') .. andStr .. il('Chair')
	elseif station == 'Work Bench and Chair' then
		return il('Work Bench') .. andStr .. il('Chair')
	-- Ecto Mist
	elseif station == 'Iron Anvil and Ecto Mist' then
		return '<span class="ib">' .. il('Iron Anvil') .. orStrInner .. il('Lead Anvil') .. '</span>' .. andStr.. ectoMist()
	elseif station == 'Bone Welder and Ecto Mist' then
		return il('Bone Welder') .. andStr .. ectoMist()
	elseif station == 'Heavy Work Bench and Ecto Mist' then
		return il('Heavy Work Bench') .. andStr.. ectoMist()
	elseif station == 'Loom and Ecto Mist' then
		return il('Loom') .. andStr.. ectoMist()
	elseif station == "Tinkerer's Workshop and Ecto Mist" then
		return il("Tinkerer's Workshop") .. andStr.. ectoMist()
	elseif station == 'Work Bench and Ecto Mist' then
		return il('Work Bench') .. andStr.. ectoMist()
	else
		return station
	end

end

local stationCell = function(station, inline)
	return stationLink(station, inline and {} or {wrap = 'y'}, l10n('station_sep_and'), l10n('station_sep_or'), l10n('station_sep_or'))
end

-- for extract.
local compactStation = function(station, formatString)
	if station == 'By Hand' then
		return
	end
	return formatString:gsub('@@@@', stationLink(station, {mode = 'image'}, "&thinsp;&amp;&thinsp;", "&thinsp;/&thinsp;", "&hairsp;/&hairsp;"))
end

local addCate, cateStr = (function()
	local cateCache = {}
	local addCate = function(station)
		cateCache[station] = true
	end
	local cateStr = function()
		local str = ''
		for station, _ in pairs(cateCache) do
			str = str .. '[[Category:'..(l10n('station_cate')[station] or tr(station, lang, 'y'))..']]'
		end
		if str ~= '' then
			str = '[[Category:'.. l10n('cate_craftable').. ']]' .. str
		end
		return str
	end
	return addCate, cateStr
end)()

local tableStart = function(caption)
	local buffer = {}
	--helper function
	local getExtColsInfo = function(field_prefix, htmlClass)
		for i = 1, math.huge do
			local v = getArg(field_prefix .. i)
			if not v then
				return i - 1
			end
			buffer[#buffer+1] = tag('th', {class = htmlClass}, v)
			i = i + 1
		end
	end --helper function

	local attr = {
		class = 'terraria cellborder recipes ',
		id = getArg('id'),
		style = getArg('css') or getArg('style'),
	}
	local sortable = getArg('sortable')
	if not (sortable == 'n' or sortable == 'no') then
		attr.class = attr.class .. 'sortable '
	end
	local class = getArg('class')
	if class then 
		attr.class = attr.class .. class
	end

	extCols_A = getExtColsInfo('col-A-')
	buffer[#buffer+1] = tag('th', {class="result"}, (getArg('header-result') or l10n('header_result')) )
	extCols_B = getExtColsInfo('col-B-')
	buffer[#buffer+1] = tag('th', {class="ingredients"}, (getArg('header-ingredients') or l10n('header_ingredients')) )
	extCols_C = getExtColsInfo('col-C-')
	if options.withStation then
		extCols_stationBefore = getExtColsInfo('station-col-before-', 'station')
		buffer[#buffer+1] = tag('th', {class='station'}, (getArg('header-station') or l10n('header_station')) )
		extCols_stationAfter = getExtColsInfo('station-col-after-', 'station')
	end
	extCols_D = getExtColsInfo('col-D-')
	str = tag('tr', nil, table.concat(buffer))
	return attr, (caption and {tag('caption', nil, caption), str} or {str})
end

local tableRow = function(row, index, status)
	local resultIndex = getArg('result-index-#'..index) 
	                 or getArg('result-index-'..row.result..'-'..(row.version or ''))
	                 or getArg('result-index-'..row.result)
	local result = table.concat({
	                 row.result, (row.resultid or ''), (row.resultimage or ''),
	                 (row.resulttext or ''), row.amount, (row.version or '')
	               }, '|')
	local stationIndex -- used later

	if status.station ~= row.station then
		status.station = row.station
		status.stationCounterPrev = status.stationCounter
		status.stationCounter = 1
	else
		status.stationCounter = status.stationCounter + 1
	end
	if status.result ~= result then
		status.result = result
		status.resultCounterPrev = status.resultCounter
		status.resultCounter = 1
	else
		status.resultCounter = status.resultCounter + 1
	end
	if status.resultIndex ~= resultIndex then
		status.resultIndex = resultIndex
		status.resultIndexCounterPrev = status.resultIndexCounter
		status.resultIndexCounter = 1
	else
		status.resultIndexCounter = status.resultIndexCounter + 1
	end

	local buffer = {}
	local extColCell = function(col, class, index, counter, needGroup, placeholder)
		local content
		local attr = {class = class}
		if index then
			local cell = index..'-row-'..col
			content = getArg(cell) or ''
			attr.colspan = getArg(cell..'-colspan')
		else
			content = ''
		end
		if counter == 1 and needGroup then
			attr.rowspan = placeholder
		end
		buffer[#buffer+1] = tag('td', attr, content)
	end
	local extColCells = function(count, prefix, station)
		if not station and (options.needGroup and status.resultIndexCounter > 1) then
			return
		end
		for i = 1, count do
			local col = prefix .. i
			if station then
				extColCell(col, 'station '..col, stationIndex, status.stationCounter, options.stationGroup, 'xxxrowspanxxx')
			else
				extColCell(col, col, resultIndex, status.resultIndexCounter, options.needGroup, 'zzzrowspanzzz')
			end
		end
	end
	extColCells(extCols_A, 'col-A-')
	if not options.needGroup or status.resultCounter == 1 then
		local attr = { class="result", ['data-sort-value'] = row.result }
		if status.resultCounter == 1 and options.needGroup then
			attr.rowspan = "yyyrowspanyyy"
		end
		buffer[#buffer+1] = tag('td', attr, resultCell(row))
	end
	extColCells(extCols_B, 'col-B-')
	buffer[#buffer+1] = tag('td', {class="ingredients"}, ingredientsCell(row.args))
	extColCells(extCols_C, 'col-C-')
	if options.withStation and (not options.stationGroup or status.stationCounter == 1) then
		station_index = getArg('station-index-'..row.station)
		extColCells(extCols_stationBefore, 'station-col-before-', true)
		buffer[#buffer+1] = tag('td', {class="station", rowspan = options.stationGroup and "xxxrowspanxxx" or nil}, stationCell(row.station))
		extColCells(extCols_stationAfter, 'station-col-after-', true)
	end
	extColCells(extCols_D, 'col-D-')
	return tag('tr', {['data-rowid']=index}, table.concat(buffer))
end

local extRows = function(isTop)
	local fieldPrefix = isTop and 'topextrow-' or 'extrow-'
	local prefix
	local buffer, notEmpty
	local extColCell = function(col, class)
		local cell = prefix..col
		local content = getArg(cell)
		notEmpty = notEmpty or content
		buffer[#buffer+1] = tag('td', {class = class or col, colspan = getArg(cell..'-colspan'), rowspan = getArg(cell..'-rowspan')}, content or '')
	end
	local extColCells = function(count, colPrefix, station)
		for i = 1, count do
			local col = colPrefix .. i
			extColCell(col, station and ('station ' .. col) )
		end
	end 

	local rows = {}
	for i = 1, math.huge do
		buffer = {}
		notEmpty = false
		prefix = fieldPrefix..i..'-'
		extColCells(extCols_A, 'col-A-')
		extColCell('col-result', 'result')
		extColCells(extCols_B, 'col-B-')
		extColCell('col-ingredients', 'ingredients')
		extColCells(extCols_C, 'col-C-')
		if options.withStation then
			extColCells(extCols_stationBefore, 'station-col-before-', true)
			extColCell('col-station', 'station')
			extColCells(extCols_stationAfter, 'station-col-after-', true)
		end
		extColCells(extCols_D, 'col-D-')
		if notEmpty then
			rows[#rows+1] = tag('tr', {['data-'..fieldPrefix..'id']=i}, table.concat(buffer))
		else
			return table.concat(rows)
		end
	end
end


local tableBody = function(result, needGroup, rootpagename, caption, expectedrows)
	local attr, buffer = tableStart(caption)
	-- top ext rows:
	buffer[#buffer+1] = extRows(true)
	-- main rows:
	local tableRows = {}
	local status = {
		station = nil,
		stationCounter = 0,
		stationCounterPrev = 0,
		result = nil,
		resultCounter = 0,
		resultCounterPrev = 0,
		resultIndex = nil,
		resultIndexCounter = 0,
		resultIndexCounterPrev = 0,
	}
	for i, row in ipairs(result) do
		tableRows[i] = tableRow(row, i, status)
		if status.stationCounter == 1 and options.stationGroup and i > 1 then
			tableRows[i-status.stationCounterPrev] = tableRows[i-status.stationCounterPrev]:gsub("xxxrowspanxxx", status.stationCounterPrev)
		end
		if status.resultCounter == 1 and options.needGroup and i > 1 then
			tableRows[i-status.resultCounterPrev] = tableRows[i-status.resultCounterPrev]:gsub("yyyrowspanyyy", status.resultCounterPrev)
		end
		if status.resultIndexCounter == 1 and options.needGroup and i > 1 then
			tableRows[i-status.resultIndexCounterPrev] = tableRows[i-status.resultIndexCounterPrev]:gsub("zzzrowspanzzz", status.resultIndexCounterPrev)
		end
		-- cate:
		if options.needCate and (options.needCate == 2 or rootpagename == tr(row.result, lang)) then
			addCate(row.station)
		end
	end
	-- rowspan for last groups
	if tableRows[1] then -- table has rows.
		if options.stationGroup then
			tableRows[#tableRows-status.stationCounter+1] = tableRows[#tableRows-status.stationCounter+1]:gsub("xxxrowspanxxx", status.stationCounter)
		end
		if options.needGroup then
			tableRows[#tableRows-status.resultCounter+1] = tableRows[#tableRows-status.resultCounter+1]:gsub("yyyrowspanyyy", status.resultCounter)
			tableRows[#tableRows-status.resultIndexCounter+1] = tableRows[#tableRows-status.resultIndexCounter+1]:gsub("zzzrowspanzzz", status.resultIndexCounter)
		end
	end
	buffer[#buffer+1] = table.concat(tableRows)
	-- ext rows:
	buffer[#buffer+1] = extRows()
	-- table end
	local rows_count = #tableRows
	attr['data-expectedRows'] = expectedrows
	attr['data-totalRows'] = rows_count
	local str = tag('table', attr, table.concat(buffer))
	if expectedrows and rows_count ~= expectedrows then
		str = str .. '[[Category:'.. l10n('cate_unexpected_rows_count') .. ']]'
	end
	if not expectedrows and rows_count == 0 then
		str = str .. '[[Category:'.. l10n('cate_no_row') .. ']]'
	end
	-- cate
	if options.needCate then
		str = str .. cateStr()
	end
	return str
end

----------------------------------------------------------------------------------

local p = {}

-- for {{recipes/register}}
p.register = function(frame)
	local args = frame:getParent().args

	-- {{{ingredients}}}
	local ingArgs = {} -- full input info, used in building ingredients cells.
	local ingredients = {} -- ingredient itemnames, for query.
	local ings = {} -- used in "group by" to eliminate duplicate rows.
	local index = 1
	while args[index*2-1] do
		local itemStr, amount = trim(args[index*2-1]), trim(args[index*2])
		local item = normalize(itemStr) -- e.g. ¦Iron Bar¦Lead Bar¦
		ingredients[index] = item
		ings[index] = item..amount
		ingArgs[index] = itemStr..'¦'..amount
		index = index + 1
	end
	table.sort(ings)

	--{{{version}}}, normalize
	version = normalizeVersion(args['version'])

	--store
	frame:callParserFunction('#cargo_store:_table=Recipes',{
		result = trim(args['result'] or ''),
		resultid = trim(args['resultid'] or ''),
		resultimage = trim(args['image'] or ''),
		resulttext = trim(args['note'] or ''), --  reuse the legacy "resulttext" field as note.
		amount = trim(args['amount'] or ''),
		version = version,
		station = normalizeStation(trim(args['station'] or '')),
		ingredients = table.concat(ingredients, '^'),
		ings = table.concat(ings, '^'),
		args = table.concat(ingArgs, '^'),
	})
end -- p.register



-- for {{recipes}}
p.query = function(frame)
	currentFrame = frame -- global frame cache
	inputArgs = frame:getParent().args -- global input args cache

	lang = frame.args['lang'] or 'en'
	l10n_table = l10n_info[lang] or l10n_info['en']

	initOptions(inputArgs)

	local where = trim(inputArgs['where'] or '')
	if where == '' then
		where = criStr(inputArgs)
	end

	-- no constraint no result.
	if where == '' then
		return frame:expandTemplate{ title='error', args={ "Recipes: No constraint", from = 'Recipes', inline = 'y'}}
	end

	-- format:

	local _title = getArg('title')
	local _expectedrows = trim(inputArgs['expectedrows'] or '')
	if _expectedrows ~= '' then
		_expectedrows = tonumber(_expectedrows)
	else
		_expectedrows = nil
	end
	local rootpagename = mw.title.getCurrentTitle().rootText

	local orderBy =  options.result_order .. ", amount DESC, version"
	if options.withStation then
		orderBy = "station, " .. orderBy -- order by station first for station grouping.
	end

	-- query
	local result = mw.ext.cargo.query('Recipes', 'result, resultid, resultimage, resulttext, amount, version, station, args', {
		where = where,
		groupBy = "resultid, result, amount, ings, version, station",
		orderBy = orderBy,
		limit = 2000,
	})
	-- format
	return tableBody(result, needGroup, rootpagename, _title, _expectedrows)
end -- p.query

-- for {{recipes/extract}}
p.extract = function(frame)
	currentFrame = frame -- global frame cache
	inputArgs = frame:getParent().args -- global input args cache

	lang = frame.args['lang'] or 'en'
	l10n_table = l10n_info[lang] or l10n_info['en']

	local where = trim(inputArgs['where'] or '')
	if where == '' then
		where = criStr(inputArgs)
	end

	-- no constraint no result.
	if where == '' then
		return frame:expandTemplate{ title='error', args={ "Recipes/extract: no constraint", from = 'Recipes', inline = 'y'}}
	end


	local result_order = trim(inputArgs['orderbyid'] or '')
	if result_order == 'y' or result_order == 'yes' then
		result_order = 'resultid'
	else
		result_order = 'result'
	end

	-- query:
	local result = mw.ext.cargo.query('Recipes', 'result, resultid, resultimage, resulttext, amount, version, station, args', {
		where = where,
		groupBy = "resultid, result, amount, version, ings",
		orderBy = result_order .. ", amount DESC, version", -- Don't order by station
		limit = 20, -- enough.
	})

	local handles = {}

	handles.compact = function()
		--default mode = compact
		local withVersion = not getArg('noversion')
		local withNote = not getArg('nonote')
		local withOne = getArg('full')
		local withResult = getArg('withresult')
		local ResultFirst = getArg('resultfirst')
		local withStation = not getArg('nostation')
		local raw = getArg('raw')
		local rows = {}
		for index, row in ipairs(result) do
			local result
			if withResult then
				local args = {mode='image'}
				if row.resultimage then
					args['image'] = row.resultimage
				end
				result = itemLink(row.result, args)
				if row.amount ~= '1' or withOne then
					result = row.amount..' '.. result
				end
				-- note text
				if withNote and (row.resulttext or '') ~= '' then
					local note_l10n = l10n('result_note') or {}
					resultnote = note_l10n[row.resulttext] or row.resulttext
					result = result .. tag('span', {class="result-note note-text small"}, resultnote)
				end
			end
			
			local ingList = explode('^', row.args)
			local args = {mode='image'}
			for i, v in ipairs(ingList) do
				local item, amount = v:match('^(.-)¦(.-)$')
				local items = split(item)
				if not items then
					if not raw then
						item = itemLink(item, args)
					end
				else
					for j, itemname in ipairs(items) do
						if not raw then
							items[j] = itemLink(itemname, args)
						else
							items[j] = item
						end
					end
					item = table.concat(items, "&hairsp;/&hairsp;")
				end
				if amount ~= '1' or withOne then
					ingList[i] = amount .. '&thinsp;' .. item
				else
					ingList[i] = item
				end
			end
			ingList = table.concat(ingList, ' + ')

			local station
			if withStation then
				station = compactStation(row.station, getArg('formatStation') or l10n('compact_format_station'))
			end

			-- format
			local formatString = getArg('format')
			if not formatString then
				if (withStation and station) then
					if withResult then 
						formatString = l10n('compact_format_recipeWithResultAndStation')
					else
						formatString = l10n('compact_format_recipeWithStation')
					end
				else
					if withResult then 
						formatString = l10n('compact_format_recipeWithResult')
					end
				end
			end
			local recipe
			if formatString then
				local replacement = { ['@station@'] = station, ['@ingredient@'] = ingList, ['@result@'] = result }
				recipe = formatString:gsub('@.-@', function(s) return replacement[s] or s end)
			else
				recipe = ingList
			end
			if withVersion and (row.version or '') ~= '' then
				local version = getVersionIconsStr(row.version)
				formatString = getArg('formatVersion') or l10n('compact_format_WithVersion')
				local replacement = { ['@version@'] = version, ['@recipe@'] = recipe }
				recipe = formatString:gsub('@.-@', function(s) return replacement[s] or s end)
			end
			rows[index] = tag('span', {class="recipe"}, recipe)
		end

		local sep = getArg('sep') or getArg('seperator') or l10n('compact_default_sep')
		if sep == 'null' or sep == 'nil' then
			sep = ''
		end

		return tag('span',{class='recipes extract compact'}, table.concat(rows, sep))
	end --handles.compact

	handles.ingredients = function()
		local withVersion = not getArg('noversion')
		local args = getArg('noversionicon') and {icons = 'n'}
		local rows = {}
		for i, row in ipairs(result) do
			local ingredient = ingredientsCell(row.args, args)
			if withVersion and (row.version or '') ~= '' then
				local version = getVersionIconsStr(row.version)
				local formatString = getArg('formatVersion') or l10n('ingredients_format_WithVersion')
				local replacement = { ['@version@'] = version, ['@ingredient@'] = ingredient }
				ingredient = formatString:gsub('@.-@', function(s) return replacement[s] or s end)
			end
			rows[i] = tag('div', {class='ingredients'}, ingredient)
		end
		local sep = getArg('sep') or getArg('seperator') or l10n('ingredients_default_sep')
		if sep == 'null' or sep == 'nil' then
			sep = ''
		end
		return tag('div', {class='recipes extract ingredients'}, table.concat(rows, sep))
	end --handles.ingredients

	handles.station = function ()
		-- only return first row.
		return result[1] and tag('span',{class='recipes extract staion'}, stationCell(result[1]['station'], true))
	end --handles.station

	handles.result = function ()
		-- only return first row.
		if not result[1] then
			return
		end
		local row = result[1]
		local result, resultid, resultimage, resultnote, amount, version = row.result, row.resultid, row.resultimage, (row.resulttext or ''), row.amount, (row.version or '')
		
		--options
		local linkResult = true
		local _link = getArg('link')
		if _link == 'n' or _link == 'no' then
			linkResult = false
		end
		showResultId = getArg('showresultid') and true or false
		local withOne = getArg('full')
		local withNote = not getArg('nonote')
		local withVersion = not getArg('noversion')
		
		-- result item first:
		local args = {nolink = not linkResult, class='multi-line'}
		if showResultId then
			args['id'] = resultid
		end
		if resultimage then
			args['image'] = resultimage
		end
		local str = itemLink(result, args)
		-- amount
		if amount ~= '1' or withOne then
			local formatString = getArg('format') or l10n('result_format_WithAmount')
			local replacement = { ['@result@'] = str, ['@amount@'] = amount }
			str = formatString:gsub('@.-@', function(s) return replacement[s] or s end)
		end
		-- note text
		if resultnote ~= '' and withNote then
			local note_l10n = l10n('result_note') or {}
			resultnote = note_l10n[resultnote] or resultnote
			str = str .. tag('span', {class="result-note note-text small"}, resultnote)
		end
		-- version text
		if withVersion and version ~= '' then
			local formatString = getArg('formatVersion') or l10n('result_format_WithVersion')
			local replacement = { ['@result@'] = str, ['@version@'] = getVersionIconsStr(version) }
			str = formatString:gsub('@.-@', function(s) return replacement[s] or s end)
		end
		return tag('span', {class='recipes extract result'}, str)
	end --handles.result

	handles.amount = function()
		-- only return first row.
		return result[1] and tag('span',{class='recipes extract amount'}, result[1]['amount'])
	end --handles.amount
	handles.resultamount = handles.amount -- alias

	handles['ingredients-buy'] = function()
		-- only process first row.
		local row = result[1]
		local value = 0
		local getItemStat = require('Module:Iteminfo').getItemStat
		for _, v in ipairs(explode('^', row.args)) do
			local item, amount = v:match('^(.-)¦(.-)$')
			value = value + getItemStat( tonumber(currentFrame:expandTemplate{ title = 'itemIdFromName', args = {item, lang='en'} }) or 0, 'value' ) * amount
		end
		return value
	end --handles['ingredients-buy']

	handles['ingredients-sell'] = function()
		-- only process first row.
		local row = result[1]
		local value = 0
		local getItemStat = require('Module:Iteminfo').getItemStat
		for _, v in ipairs(explode('^', row.args)) do
			local item, amount = v:match('^(.-)¦(.-)$')
			value = value + math.floor(getItemStat( tonumber(currentFrame:expandTemplate{ title = 'itemIdFromName', args = {item, lang='en'} }) or 0, 'value' )/5) * amount
		end
		return value
	end --handles['ingredients-sell']

	-- output:
	local mode = getArg('mode') or 'compact'
	if handles[mode] then
		return (handles[mode])()
	else
		return frame:expandTemplate{ title='error', args={ "Recipes/extract: Invalid mode", from = 'Recipes', inline = 'y'}}
	end
end -- p.extract

-- count; return int; for {{recipes/count}}
p.count = function(frame)
	local args = frame:getParent().args
	local where = trim(args['where'] or '')
	if where == '' then
		where = criStr(args)
	end
	-- no constraint no result.
	if where == '' then
		return 0
	end
	-- query: since we must use group by to eliminate duplicates, so we can not use COUNT() to get row count directly. and COUNT(DISTICT ..) will also not get correct result due to HOLD keyword.
	local result = mw.ext.cargo.query('Recipes', 'resultid', {
		where = where,
		groupBy = "resultid, result, amount, ings, version",
		limit = 2000,
	})
	-- count
	return #result
end -- p.count

-- return "yes" or ""; for {{recipes/exist}}
p.exist = function(frame)
	local args = frame:getParent().args
	local where = trim(args['where'] or '')
	if where == '' then
		where = criStr(args)
	end
	-- no constraint no result.
	if where == '' then
		return
	end
	-- query:
	local result = mw.ext.cargo.query('Recipes', 'result', {
		where = where,
		limit = 1, -- enough.
	})
	-- output
	return result[1] and 'yes'
end -- p.exist

return p
Advertisement