Module:Exclusive

From Terraria Wiki
Jump to navigation Jump to search
Lua.svg Documentation The documentation below is transcluded from Module:Exclusive/doc. (edit | history)

This module is the core of the dynamic platform exclusivity system.

The basic function to invoke from templates is getInfo, which queries the exclusivity information database and stores the result to a set of dplvars. These are named in the format ex_<platform>, e.g. ex_d for Desktop, and they are either empty or set to y. The function does not produce any output.

The two other functions, eicons and simpleEicons, return the HTML code for the icons denoting platform exclusivity. The first function is intended to be used only by the {{eicons}} template, the second function can be used from any other module that includes platform exclusivity icons (such as Module:Item).

Important note: This module relies entirely on the database Module:Exclusive/data. The database is not updated automatically and instead requires periodic manual updates. Please see its documentation for instructions.


---Holds the tables with the l10n information for the different languages, taken from the l10n submodule.
local l10n_data = mw.loadData('Module:Exclusive/l10n')

---Database with exclusivity info.
---luacache.get() is relative slow, therefore we load data from luacache only once per page.
local exclusive_info = mw.loadData('Module:Exclusive/loaddata')

local bit32 = require('bit32')
local trim = mw.text.trim

---Default content language of the wiki (i.e. `$wgLanguageCode`, not the value of the
---`uselang` URL parameter, and not the user's language preference setting).
local contentLanguage = mw.getContentLanguage()

---Holds the arguments from the template call.
local args_table

---The current language. Determines which l10n table to use.
local lang

---Return the l10n string associated with the `key`.
---@param key string
---@return string
local function l10n(key)
	if l10n_data[lang] then
		return l10n_data[lang][key] or l10n_data['en'][key]
	else
		return l10n_data['en'][key]
	end
end

---Return a trimmed version of the value of the template parameter with the specified `key`.
---Return `nil` if the parameter is empty or unset.
---@param key string|number
---@return string|nil
local function getArg(key)
	local value = trim(args_table[key] or '')
	return (value ~= '') and value or nil
end

---Convert a string of parameters in a `@param1:value^@param2:value^` format to a table.
---@param paramstr string
---@return table
local function parse(paramstr)
	local args = {}
	for s in string.gmatch(paramstr, '%b@^') do
		local k,v = string.match(s, '^@(.-):(.*)^$')
		args[k] = v
	end
	return args
end

---Split the `str` on each `div` in it and return the result as a table.
---This is much much faster then `mw.text.split`.
---Credit: http://richard.warburton.it.
---@param div string
---@param str string
---@return table|boolean
local function explode(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
		arr[#arr + 1] = string.sub(str,pos,st-1) -- Attach chars left of current divider
		pos = sp + 1 -- Jump past current divider
	end
	arr[#arr + 1] = string.sub(str,pos) -- Attach chars right of last divider
	return arr
end

---Return the integer defined in the database for the specified `page`.
---Perform some standardization on the `page` for that first.
---@param page string
---@return number
local function readFromDb(page)
	-- standardize pagename: remove section parts ('x#section' -> 'x') and replace underscores with spaces
	page = contentLanguage:ucfirst(string.gsub(string.gsub(page or '', '#.*', ''), '_', ' '))
	return exclusive_info[page] or 0
end

---Override the exclusivity information in `info`
---with the content of `jdcom3`.
---@param info number
---@param jdcom3 string Expected format: `<j>:<d>:<c>:<o>:<m>:<3>`, with each platform either a Boolean string ("y", "0", etc.) or an empty string
---@return number
local function override(info, jdcom3)
	for k, v in pairs(explode(':', jdcom3)) do
		if v ~= '' then
			if v == '1' or v == 'y' or v == 'yes' then
				info = bit32.replace(info, 1, k-1)
			elseif v == '0' or v == 'n' or v == 'no' then
				info = bit32.replace(info, 0, k-1)
			end
		end
	end
	return info

	-- Example to demonstrate the behavior of this function:
	-- Goal: Change "dcom" to "dco3".
	-- info = 30 ("dcom"), jdcom3 = "::::no:yes"
	-- decimal 30 = binary 011110
	-- The first "no" is at index 4 in the jdcom3 string, so
	-- replace the corresponding bit with a 0: 011110 -> 001110.
	-- The "yes" is at index 5, so replace the bit at index 5
	-- with a 1: 001110 -> 101110.
	-- The result is info = 46 ("dco3").
end

---Main function to retrieve exclusivity information.
---@param page string The entity to get the info about.
---@param invert boolean Whether to invert the exclusivity info.
---@param pagenot string The entity whose exclusivity info to subtract from the main one's.
---@param jdcom3 string Manual exclusivity info to override the fetched one with.
---@return number info An integer that holds the exclusivity information.
local function getInfo(page, invert, pagenot, jdcom3)
	local info = 0

	-- A piece of exclusivity information is a set of Boolean values, one
	-- for each platform. This is represented as bits of the `info` integer.
	-- Each platform (Japanese console, Desktop, Console, Old-gen console,
	-- Mobile, and 3DS – "jdcom3") is assigned one bit, in this order.
	-- This means that, for instance, an `info` value of 2 would represent
	-- Desktop-only exclusivity ("d"):
	-- decimal  2 = binary 000010
	--                     3mocdj  -> "d"
	-- Similarly, an `info` value of 40 would represent Old-gen and 3DS exclusivity ("o3"):
	-- decimal 40 = binary 101000
	--                     3mocdj -> "o3"
	-- See Module:Exclusive/data for a quick overview of all values.

	-- This system allows using bitwise operations (https://en.wikipedia.org/wiki/Bitwise_operation)
	-- instead of the historically used string processing, resulting in much lower script execution times.

	-- get info about page
	if page then
		info = readFromDb(page)
		if invert then
			-- invert jdcom3 and set j=0 (always force-off Japanese console when inverting)
			-- (3E in hexadecimal is 111110 in binary, i.e. "dcom3")
			info = bit32.band(bit32.bnot(info), 0x3E)
		end
		if pagenot then
			-- exclude some versions, depending on pagenot
			local info_not = readFromDb(pagenot)
			info = bit32.band(info, bit32.bnot(info_not))
		end

		-- The "invert" and "pagenot" functionalities above utilize
		-- bit masking (https://en.wikipedia.org/wiki/Mask_(computing)).
		-- The following example demonstrates the operations:
		-- 1. assume info=22 ("dcm", binary 010110)
		-- 2. invert:
		--    2a. not(010110) = 101001 ("jo3", the inverse of "dcm")
		--    2b. and(101001, 111110) = 101000 ("o3", forced-off "j")
		-- 3. assume info_not=8 ("o", binary 001000)
		--    3a. not(001000) = 110111
		--    3b. and(101000, 110111) = 100000 ("3")
		-- An initial exclusivity info of "dcm" was inverted to "o3",
		-- then "o" was subtracted from it, resulting in the final
		-- exclusivity information of "3".
	end

	-- override if needed
	if jdcom3 == nil or jdcom3 == ':::::' then
		return info
	else
		return override(info, jdcom3)
	end
end

---Return an HTML span tag whose `class` attribute is set
---according to the exclusivity `info`.
---@param info number
---@param _small boolean Whether to add the "s" class, for small icons
---@return string
local function eicons(info, _small)
	local class = _small and 'eico s' or 'eico'
	local hovertext

	if bit32.btest(info, 0x01) then
		-- Japanese console is set, so simply display that
		-- ("j" is always alone or not set at all – "dcj", for instance, doesn't exist)
		return '<span class="'..class..' j" title="'..l10n('text_j')..'"></span>';
	end

	-- for each platform of dcom3, add the class and load the hovertext if the platform if set
	-- (e.g. for info=50 ("dm3"), append "i1 i4 i5" to the class and load the
	-- "text_1", "text_4", and "text_5" l10n strings)
	local v = {}
	for i = 1, 5 do -- 0 is "j"
		if bit32.btest(info, 2^i) then
			class = class .. " i" .. i
			v[#v+1] = l10n('text_' .. i)
		end
	end
	local hovertext = mw.text.listToText(v, l10n('list_separator'), l10n('list_conjunction'))
	hovertext = string.format(contentLanguage:convertPlural(#v, l10n('version_plural_forms')), hovertext)
	return '<span class="'..class..'" title="'..hovertext..'"><b></b><i></i><span>('..hovertext..')</span></span>';
end

---Check if the `infoToCheck` integer is a valid number for output.
---It is considered invalid if it represents an empty exclusivity ("")
---or a "full" exclusivity, i.e. all platforms being set ("dcom3" or "jdcom3").
---@param infoToCheck number
---@return boolean
local function infoIsInvalid(infoToCheck)
	return infoToCheck == 0x00 or infoToCheck == 0x3E or infoToCheck == 0x3F
end

-----------------------------------------------------------------
-- main return object
return {

-- for templates; get all exclusive info and set it in dplvars.
-- parameters: $1 = pagename
getInfo = function(frame)
	args_table = frame.args -- cache

	local info = getInfo(getArg(1), getArg('invert'), getArg('pagenot'))
	if infoIsInvalid(info) then
		frame:callParserFunction{ name = '#dplvar:set', args = {
			'ex_j', '',
			'ex_d', '',
			'ex_c', '',
			'ex_o', '',
			'ex_m', '',
			'ex_3', '',
			'ex_cached', 'y'
		} }
	else
		frame:callParserFunction{ name = '#dplvar:set', args = {
			'ex_j', bit32.btest(info, 2^0) and 'y' or '',
			'ex_d', bit32.btest(info, 2^1) and 'y' or '',
			'ex_c', bit32.btest(info, 2^2) and 'y' or '',
			'ex_o', bit32.btest(info, 2^3) and 'y' or '',
			'ex_m', bit32.btest(info, 2^4) and 'y' or '',
			'ex_3', bit32.btest(info, 2^5) and 'y' or '',
			'ex_cached', 'y'
		} }
	end
end,

-- for {{eicons}}
eicons = function(frame)
	args_table = parse(frame.args[1])-- cache
	lang = getArg('lang') -- set lang for l10n
	local info = getInfo(getArg('page'), getArg('invert'), getArg('pagenot'), getArg('jdcom3'))
	if infoIsInvalid(info) then
		return frame:expandTemplate{ title = 'error', args = { l10n('eicons_error_text'), l10n('eicons_error_cate'), from = 'Eicons' } }
	end
	return eicons(info, getArg('small'))
end,

-- simplified version of the eicons function above, for other modules such as Module:Item
simpleEicons = function(page, language, small)
	local info = getInfo(page)
	if infoIsInvalid(info) then
		return ''
	end
	lang = language -- set lang for l10n
	return eicons(info, small)
end,

}