PC Gaming Shelter
An archive dedicated to preserving PC Gaming history and more

Module:Universe

From PC Gaming Shelter
Revision as of 11:32, 6 August 2026 by WikiVisor (talk | contribs)

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

local utils = require("Module:Utils")

local p = {}

local function getCurrentGame()
	local title = mw.title.getCurrentTitle()
	local game = title.rootText
	local resource = title.subpageText
	local data = {
		["game"] = game,
		["resource"] = resource,
	}
	return data
end

local function setDisplayTitle(frame, title)
	local displayTitle = frame:preprocess(string.format("{{DISPLAYTITLE:%s|noreplace, noerror}}", title))
	return displayTitle
end

local function escapePattern(value)
	return value:gsub("([^%w])", "%%%1")
end

local function removeGameFromLabels(results, game)
	local gamePattern = escapePattern(game)

	for index, link in ipairs(results) do
		results[index] = link:gsub("|" .. gamePattern .. "%s+", "|")
	end

	return results
end

local function renameLabels(results)
	local labels = {
		Mods = "Mod Archive",
		Players = "Player Roster",
		Clans = "Clan History",
	}

	for i, link in ipairs(results) do
		results[i] = link:gsub("|([^%]]+)%]%]", function(label)
			return "|" .. (labels[label] or label) .. "]]"
		end)
	end

	return results
end

local function sortSubpages(results)
	local order = {
		Mods = 1,
		Players = 2,
		Clans = 3,
	}

	table.sort(results, function(a, b)
		-- Extract the final subpage name from the link target.
		local targetA = a:match("^%[%[:.-/([^/|%]]+)|")
		local targetB = b:match("^%[%[:.-/([^/|%]]+)|")

		local rankA = order[targetA] or math.huge
		local rankB = order[targetB] or math.huge

		if rankA ~= rankB then
			return rankA < rankB
		end

		-- Keep all remaining subpages alphabetically ordered.
		return (targetA or a) < (targetB or b)
	end)

	return results
end

local function getSubpages(page)
	local subpages = mw.smw.ask({
		"[[Has parent page::" .. page .. "]][[Modification date::+]]",
	}) or {}

	local subpageTable = {}

	for _, subpage in ipairs(subpages) do
		local link = subpage[1]

		if link then
			table.insert(subpageTable, link)
		end
	end

	local allGames = string.format("[[%s|%s]]", "Games", "All Games")
	table.insert(subpageTable, allGames)

	removeGameFromLabels(subpageTable, page)
	renameLabels(subpageTable)
	sortSubpages(subpageTable)

	if #subpageTable == 0 then
		return ""
	end

	local html = mw.html.create()

	local universe = html:tag("div"):attr("id", "universeMenu"):wikitext(" | " .. table.concat(subpageTable, " | "))

	return tostring(html)
end

local function formatCellValue(value)
	if value == nil then
		return ""
	end

	if type(value) == "table" then
		local values = {}

		for _, item in ipairs(value) do
			table.insert(values, tostring(item))
		end

		return table.concat(values, "<br>")
	end

	return tostring(value)
end

local function formatDownloadLinks(value)
	if type(value) ~= "table" then
		return formatCellValue(value)
	end

	local links = {}

	for _, link in ipairs(value) do
		local formattedLink = formatCellValue(link)

		if formattedLink ~= "" then
			table.insert(links, formattedLink)
		end
	end

	return table.concat(links, "<br>")
end

local function renderModTable(content)
	local wrapper = mw.html.create("div")
	wrapper:attr("id", "assets")

	local tableNode = wrapper:tag("table")
	tableNode:addClass("smwtable-clean")

	for _, row in ipairs(content or {}) do
		local tr = tableNode:tag("tr")

		tr:tag("td"):wikitext(formatCellValue(row[1]))
		tr:tag("td"):wikitext(formatCellValue(row["Category"]))
		tr:tag("td"):wikitext(
			formatCellValue(row["Has map type"]),
			formatCellValue(row["Has skin type"]),
			formatCellValue(row["Has mod type"]),
			formatCellValue(row["Has utility type"])
		)
		tr:tag("td"):wikitext(formatCellValue(row["Has game mode"]))
		tr:tag("td"):wikitext(formatCellValue(row["Has developer"]))
		tr:tag("td"):wikitext(formatCellValue(row["Has release date"]))
		tr:tag("td"):wikitext(formatCellValue(row["Has status"]))
		tr:tag("td"):wikitext(formatCellValue(row["Has short description"]))
		tr:tag("td"):wikitext(formatCellValue(row["Works on game version"]))
		tr:tag("td"):wikitext(formatDownloadLinks(row["Download links"]))
		tr:tag("td"):wikitext(formatCellValue(row["Related game"]))
	end

	return wrapper
end

local function renderPlayerTable(content, panes)
	local wrapper = mw.html.create("div")
	wrapper:attr("id", "players"):attr("data-pane-columns", panes)

	local tableNode = wrapper:tag("table")
	tableNode:addClass("smwtable-clean")

	for _, row in ipairs(content or {}) do
		local clanValues = {}
		local roleValues = {}
		local clanAndRoleValues = {}

		local member = row["Member of"]

		if member then
			-- SMW may return either one value or a table of values.
			local memberships = type(member) == "table" and member or { member }

			for _, clan in ipairs(memberships) do
				local formattedClan = formatCellValue(clan)

				table.insert(clanValues, string.format("<div>%s</div>", formattedClan))

				local query = {
					"[[-Has subobject::" .. row[1] .. "]]",
					"[[Member of::" .. clan .. "]]",
					"?Has role#-=role",
					mainlabel = "-",
				}

				local results = mw.smw.ask(query)
				local clanRoles = {}

				local function addRoles(value)
					if value == nil or value == "" then
						return
					end

					for role in mw.text.gsplit(tostring(value), ",", true) do
						local normalizedRole = mw.text.trim(role)

						if normalizedRole ~= "" then
							table.insert(clanRoles, formatCellValue(normalizedRole))
						end
					end
				end

				for _, resultRow in ipairs(results or {}) do
					local resultRoles = resultRow.role

					if type(resultRoles) == "table" then
						for _, roleValue in ipairs(resultRoles) do
							addRoles(roleValue)
						end
					else
						addRoles(resultRoles)
					end
				end

				if #clanRoles > 0 then
					local roleDivs = "<div>" .. table.concat(clanRoles, "</div><div>") .. "</div>"

					table.insert(roleValues, roleDivs)

					table.insert(
						clanAndRoleValues,
						string.format("<div>%s (%s)</div>", formattedClan, table.concat(clanRoles, ", "))
					)
				else
					table.insert(roleValues, "<div>—</div>")

					table.insert(clanAndRoleValues, string.format("<div>%s</div>", formattedClan))
				end
			end
		end

		local hasStatus = row["Has status"]
		local activeClass = "text-danger"
		if hasStatus == "Active" then
			activeClass = "text-success"
		end

		local tr = tableNode:tag("tr")
		tr:tag("td"):wikitext(formatCellValue(row[1])):addClass(activeClass)
		tr:tag("td"):wikitext(formatCellValue(row["Plays game"]))
		tr:tag("td"):wikitext(table.concat(clanValues, ""))
		tr:tag("td"):wikitext(table.concat(roleValues, ""))
		tr:tag("td"):wikitext(table.concat(clanAndRoleValues, ""))
		tr:tag("td"):wikitext(hasStatus)
	end

	return wrapper
end

local function renderClanPlayerTable(content, clanname)
	local title = mw.title.getCurrentTitle()
	local clanpage = clanname

	local wrapper = mw.html.create("div")
	wrapper:attr("id", "players"):attr("data-pane-columns", "3, 5")

	local tableNode = wrapper:tag("table")
	tableNode:addClass("smwtable-clean")

	for _, row in ipairs(content or {}) do
		local clanValues = {}
		local roleValues = {}

		local member = row["Member of"]

		if member then
			local memberships = { clanpage }

			for _, clan in ipairs(memberships) do
				local formattedClan = formatCellValue(clan)

				if formattedClan == clanpage then
					table.insert(clanValues, string.format("<div>%s</div>", formattedClan))

					local query = {
						"[[-Has subobject::" .. row[1] .. "]]",
						"[[Member of::" .. clan .. "]]",
						"?Has role#-=role",
						mainlabel = "-",
					}

					local results = mw.smw.ask(query)

					local clanRoles = {}

					local function addRoles(value)
						if value == nil or value == "" then
							return
						end

						for role in mw.text.gsplit(tostring(value), ",", true) do
							local normalizedRole = mw.text.trim(role)

							if normalizedRole ~= "" then
								table.insert(clanRoles, formatCellValue(normalizedRole))
							end
						end
					end

					for _, resultRow in ipairs(results or {}) do
						local resultRoles = resultRow.role

						if type(resultRoles) == "table" then
							for _, roleValue in ipairs(resultRoles) do
								addRoles(roleValue)
							end
						else
							addRoles(resultRoles)
						end
					end

					if #clanRoles > 0 then
						local roleDivs = table.concat(clanRoles, ", ")
						table.insert(roleValues, roleDivs)
					else
						table.insert(roleValues, "—")
					end
				end

				local hasStatus = row["Has status"]
				local activeClass = "text-danger"
				if hasStatus == "Active" then
					activeClass = "text-success"
				end

				local tr = tableNode:tag("tr")
				tr:tag("td"):wikitext(formatCellValue(row[1])):addClass(activeClass)
				tr:tag("td"):wikitext(formatCellValue(row["Plays game"]))
				tr:tag("td"):wikitext(table.concat(clanValues, ""))
				tr:tag("td"):wikitext(table.concat(roleValues, ""))
				tr:tag("td"):wikitext(table.concat(roleValues, ""))
				tr:tag("td"):wikitext(hasStatus)
			end
		end
	end

	return wrapper
end

local function renderClanTable(content)
	local wrapper = mw.html.create("div")
	wrapper:attr("id", "clans")

	local tableNode = wrapper:tag("table")
	tableNode:addClass("smwtable-clean")

	for _, row in ipairs(content or {}) do
		local tr = tableNode:tag("tr")

		tr:tag("td"):wikitext(formatCellValue(row[1]))
		tr:tag("td"):wikitext(formatCellValue(row["Related game"]))
		tr:tag("td"):wikitext(formatCellValue(row["Community links"]))
		tr:tag("td"):wikitext(formatCellValue(row["Has short description"]))
	end

	return wrapper
end

function p.Mods(frame)
	local data = getCurrentGame()
	local game = data.game or "+"
	local resource = data.resource

	mw.smw.set({
		["Has parent page"] = game,
	})

	local query = {
		"[[Category:Mods||Maps||Skins||Utilities]][[Related game::" .. game .. "]]",
		"?Related game",
		"?Category",
		"?Has mod type",
		"?Has map type",
		"?Has skin type",
		"?Has utility type",
		"?Has game mode",
		"?Has developer",
		"?Has release date#-F[Y]",
		"?Has status",
		"?Has short description",
		"?Works on game version",
		"?Download links",
	}
	local content = mw.smw.ask(query)

	local forminput = frame:callParserFunction({
		name = "#forminput",
		"",
		args = {
			"form=Mod",
			"query string=Mod[Related game]=" .. game,
			"button text=Create or Edit Mod",
			"autofocus=no",
		},
	})

	setDisplayTitle(frame, string.format("%s %s", game, resource))

	local html = mw.html.create()
	html:wikitext(getSubpages(game))
	html:wikitext(forminput)
	html:node(renderModTable(content))

	return html
end

function p.allMods(frame)
	local query = {
		"[[Category:Mods||Maps||Skins||Utilities]][[Related game::+]]",
		"?Related game",
		"?Category",
		"?Has mod type",
		"?Has map type",
		"?Has skin type",
		"?Has utility type",
		"?Has game mode",
		"?Has developer",
		"?Has release date#-F[Y]",
		"?Has status",
		"?Has short description",
		"?Works on game version",
		"?Download links",
	}
	local content = mw.smw.ask(query)

	local html = mw.html.create()
	html:node(renderModTable(content, true))

	return html
end

function p.Players(frame)
	local data = getCurrentGame()
	local game = data.game
	local resource = data.resource

	mw.smw.set({
		["Has parent page"] = game,
	})

	local query = {
		"[[Category:Players]][[Plays game::" .. game .. "]]",
		"?Plays game",
		"?Member of",
		"?Has status",
	}

	local content = mw.smw.ask(query)

	local forminput = frame:callParserFunction({
		name = "#forminput",
		"",
		args = {
			"form=Player",
			"query string=Player[Plays game]=" .. game,
			"button text=Create or Edit Player Entry",
			"autocomplete on category=Players",
			"autofocus=no",
		},
	})

	setDisplayTitle(frame, string.format("%s %s", game, resource))

	local html = mw.html.create()
	html:wikitext(getSubpages(game))
	html:wikitext(forminput)
	html:node(renderPlayerTable(content, "2, 3, 5"))

	return html
end

function p.allPlayers(frame)
	local query = {
		"[[Category:Players]][[Plays game::+]]",
		"?Plays game",
		"?Member of",
		"?Has status",
	}

	local content = mw.smw.ask(query)

	local html = mw.html.create()
	html:node(renderPlayerTable(content, "1, 2, 3, 5"))

	return html
end

function p.clanPlayers(frame)
	local title = mw.title.getCurrentTitle()
	local clan = title.fullText

	local query = {
		"[[Member of::" .. clan .. "]][[Modification date::+]]",
		"?Plays game",
		"?Member of",
		"?Has status",
	}

	local content = mw.smw.ask(query)

	local html = mw.html.create()
	html:node(renderClanPlayerTable(content, clan))

	return html
end

function p.Clans(frame)
	local data = getCurrentGame()
	local game = data.game
	local resource = data.resource

	mw.smw.set({
		["Has parent page"] = game,
	})

	local query = {
		"[[Category:Communities]]" .. "[[Related game::" .. game .. "]]",
		"?Related game",
		"?Community links",
		"?Has short description",
	}

	local content = mw.smw.ask(query)

	local forminput = frame:callParserFunction({
		name = "#forminput",
		"",
		args = {
			"form=Community",
			"query string=Community[Related game]=" .. game,
			"button text=Create or Edit Community Entry",
			"autocomplete on category=Communities",
			"autofocus=no",
		},
	})

	local html = mw.html.create()
	html:wikitext(getSubpages(game))
	html:wikitext(forminput)
	html:node(renderClanTable(content))

	return html
end

function p.allClans(frame)
	local args = frame.args
	local hasStatus = args['Has status']
	local query = {
		"[[Category:Communities]][[Has status::" .. hasStatus .. "]]",
		"?Related game",
		"?Community links",
		"?Has short description",
	}

	local content = mw.smw.ask(query)

	local forminput = frame:callParserFunction({
		name = "#forminput",
		"",
		args = {
			"form=Community",
			"query string=Community[Has status]=" .. hasStatus,
			"button text=Create or Edit Community Entry",
			"autocomplete on category=Communities",
			"autofocus=no",
		},
	})

	local html = mw.html.create()
	html:wikitext(forminput)
	html:node(renderClanTable(content))

	return html
end

return p