Module:Exclusive

From wizzypedia
Jump to navigation Jump to search

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

---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.
local exclusive_info = mw.loadData('Module:Exclusive/data')

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 `jdcom3st`.
---@param info number
---@param jdcom3st string Expected format: `<j>:<d>:<c>:<o>:<m>:<3>:<s>:<t>`, with each platform either a Boolean string ("y", "0", etc.) or an empty string
---@return number
local function override(info, jdcom3st)
	for k, v in pairs(explode(':', jdcom3st)) 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"), jdcom3st = "::::no:yes::"
	-- decimal 30 = binary 00011110
	-- The first "no" is at index 4 in the jdcom3st string, so
	-- replace the corresponding bit with a 0: 00011110 -> 00001110.
	-- The "yes" is at index 5, so replace the bit at index 5
	-- with a 1: 00001110 -> 00101110.
	-- 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 jdcom3st 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, jdcom3st)
	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, 3DS,
	-- Switch, and TModLoader – "jdcom3st") 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 00000010
	--                     ts3mocdj  -> "d"
	-- Similarly, an `info` value of 40 would represent Old-gen and 3DS exclusivity ("o3"):
	-- decimal 40 = binary 00101000
	--                     ts3mocdj -> "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 formerly used string processing, resulting in much lower script execution times.

	-- get info about page
	if page then
		info = readFromDb(page)
		if invert then
			-- invert jdcom3st and set j=0 (always force-off Japanese console when inverting)
			-- (0xFE is 11111110 in binary, i.e. "dcom3st")
			info = bit32.band(bit32.bnot(info), 0xFE)
		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=214 ("dcmst", binary 11010110)
		-- 2. invert:
		--    2a. not(11010110) = 00101001 ("jo3", the inverse of "dcmst")
		--    2b. and(00101001, 11111110) = 00101000 ("o3", forced-off "j")
		-- 3. assume info_not=8 ("o", binary 00001000)
		--    3a. not(00001000) = 11110111
		--    3b. and(00101000, 11110111) = 00100000 ("3")
		-- An initial exclusivity info of "dcmst" was inverted to "o3",
		-- then "o" was subtracted from it, resulting in the final
		-- exclusivity information of "3".
	end

	-- override if needed
	if jdcom3st == nil or jdcom3st == ':::::::' then
		return info
	else
		return override(info, jdcom3st)
	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, 7 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>';
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 ("dcom3st" or "jdcom3st").
---@param infoToCheck number
---@return boolean
local function infoIsInvalid(infoToCheck)
	return infoToCheck == 0x00 or infoToCheck == 0xFE or infoToCheck == 0xFF
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_s', '',
			'ex_t', '',
			'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_s', bit32.btest(info, 2^6) and 'y' or '',
			'ex_t', bit32.btest(info, 2^7) and 'y' or '',
			'ex_cached', 'y'
		} }
	end
end,

-- for {{eicons}}
eicons = function(frame)
	args_table = parse(frame.args[1])-- cache

	local info = getInfo(getArg('page'), getArg('invert'), getArg('pagenot'), getArg('jdcom3st'))
	if infoIsInvalid(info) then
		return frame:expandTemplate{ title = 'error', args = { l10n('eicons_error_text'), l10n('eicons_error_cate'), from = 'Eicons' } }
	end
	lang = getArg('lang') -- set lang for l10n
	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,

}