Module:SMAPI compatibility
Revision as of 04:49, 20 February 2021 by Pathoschild (talk | contribs) (add support for Curseforge & ModDrop, add new metadata parameters, drop legacy backwards-compatibility output, tweak default summaries)
This module provides the table structure and data for the Modding:Mod compatibility list.
Examples
Compatible mod
{{#invoke:SMAPI compatibility|entry |name = Lookup Anything |author = Pathoschild |id = Pathoschild.LookupAnything |nexus = 541 |github = Pathoschild/StardewMods }}
mod name | author | compatibility | broke in | source | |
---|---|---|---|---|---|
[ Lookup Anything] | Pathoschild | ✓ use latest version. | source | # |
Broken mod
{{#invoke:SMAPI compatibility|entry |name = Lookup Anything |author = Pathoschild |id = Pathoschild.LookupAnything |nexus = 541 |github = Pathoschild/StardewMods |summary = |broke in = Stardew Valley 1.2 }}
mod name | author | compatibility | broke in | source | |
---|---|---|---|---|---|
[ Lookup Anything] | Pathoschild | ↻ broken, not updated yet. | Stardew Valley 1.2 | source | # |
Unofficial update
For an unofficial update, use the broken-mod template and add these under the other fields:
|unofficial url = https://community.playstarbound.com/attachments/201345000 |unofficial version = 1.18.2-unofficial.1-example
mod name | author | compatibility | broke in | source | |
---|---|---|---|---|---|
[ Lookup Anything] | Pathoschild | ⚠ broken, use unofficial version (1.18.2-unofficial.1-example). | Stardew Valley 1.2 | source | # |
Usage
Limitations
The name, author, and id arguments are comma-separated. If the actual value contains a comma, use ,
instead.
Main fields (shown above)
field | purpose |
---|---|
name
|
The normalised display name for the mod. Delimit alternate names with commas. |
author
|
The name of the author, as shown on Nexus or in its manifest.json file. Delimit alternate names with commas. |
id
|
The latest unique mod ID, as listed in its manifest.json file. Delimit alternate/older IDs with commas (ideally in latest to oldest order). For very old mods with no ID, use none to disable validation checks. |
nexus
|
The mod's unique ID on Nexus (if any). This is the number in the mod page's URL. |
github
|
The mod's GitHub repository in the form owner/repo. |
summary
|
Specify custom notes or instructions about the mod's compatibility. Should usually be blank. |
broke in
|
The SMAPI or Stardew Valley update which broke this mod (if applicable). |
Other fields
field | purpose |
---|---|
status
|
Whether the mod is compatible with the latest versions of Stardew Valley and SMAPI (see #Valid statuses). If not specified, it defaults to unofficial if an unofficial URL is given, else broken if broke in is specified, else ok .
|
unofficial url
|
A page URL where the player can download an unofficial update, if any. |
unofficial version
|
The unofficial update's version number, if any. |
chucklefish
|
The mod's ID in the Chucklefish mod repository. |
curse
|
The mod's project ID and key in the CurseForge mod repository. The ID is shown on the mod page next to "Project ID", and the key is shown in the mod page's URL. This must be in the form id,key .
|
moddrop
|
The mod's ID in the ModDrop mod repository. |
url
|
The arbitrary mod URL, if not on a known mod site. Avoid if possible, since this makes crossreferencing more difficult. |
source
|
An arbitrary source code URL, if not on GitHub. Avoid if possible, since this makes crossreferencing more difficult. |
warnings
|
Text explaining additional compatibility warnings about the mod (e.g., not compatible with Linux/Mac). |
content pack for
|
The name of the mod which loads this content pack. |
dev note
|
Special notes intended for developers who maintain unofficial updates or submit pull requests. |
Valid statuses
status | description |
---|---|
ok
|
The mod is compatible. This is the default and doesn't need to be specified. Default summary: use latest version. |
optional
|
The mod is compatible, if you use an optional download on the mod page. Default summary: use optional download.[1] |
unofficial
|
The mod is compatible using an unofficial update. There's no need to specify this; if you also set unofficial url and unofficial version, you can remove the status field. |
workaround
|
The mod isn't compatible, but the player can fix it or there's a good alternative. A summary should be provided manually. If you also set unofficial url and unofficial version, you can remove the status field. |
broken
|
The mod isn't compatible. The message depends on whether the source link is set.Default summary: broken, not updated yet or broken, not open-source. |
abandoned
|
The mod is no longer maintained by the author, and an unofficial update or continuation is unlikely. This should only be used when the author has definitively abandoned the mod (either explicitly, or by removing the mod page or downloads). Default summary: no longer maintained. |
obsolete
|
The mod is no longer needed and should be removed. |
unknown
|
The mod's compatibility status hasn't been tested. This should only be used as a placeholder (e.g., when adding a new beta), it should never be used long since that defeats the purpose of the compatibility list. |
local p = {}
local private = {}
--##########
--## Public functions
--##########
-- Start a SMAPI compatibility table.
-- @test mw.log(p.header())
function p.header()
return
'<table class="wikitable plainlinks">'
.. "<tr><th>mod name</th><th>author</th><th>compatibility</th><th>broke in</th><th>source</th><th> </th></tr>";
end
-- End a SMAPI compatibility table.
-- @test mw.log(p.footer())
function p.footer()
return '</table>'
end
--- Render a mod row in the SMAPI compatibility table.
-- @param frame The arguments passed to the script.
-- @test mw.log(p.entry({ args = { name="Lookup Anything, LookupAnything", author="Pathoschild, Pathos", id="Pathoschild.LookupAnything, LookupAnything", ["nexus id"]="541", ["cf id"]="4250", ["github"]="Pathoschild/StardewMods", warnings="warning A, warning B", links="https://google.ca" }}))
function p.entry(frame)
-- read input args
local names = private.parseCommaDelimited(frame.args["name"] or '')
local authors = private.parseCommaDelimited(frame.args["author"] or '')
local ids = private.parseCommaDelimited(frame.args["id"] or '')
local nexusId = private.emptyToNil(frame.args["nexus id"])
local github = private.emptyToNil(frame.args["github"])
local summary = private.emptyToNil(frame.args["summary"])
local brokeIn = private.emptyToNil(frame.args["broke in"])
local status = private.emptyToNil(frame.args["status"])
local unofficialVersion = private.emptyToNil(frame.args["unofficial version"])
local unofficialUrl = private.emptyToNil(frame.args["unofficial url"])
local chucklefishId = private.emptyToNil(frame.args["cf id"])
local curseforgeId = private.emptyToNil(frame.args["curseforge id"])
local curseforgeKey = private.emptyToNil(frame.args["curseforge key"])
local moddropId = private.emptyToNil(frame.args["moddrop id"])
local customUrl = private.emptyToNil(frame.args["url"])
local customSource = private.emptyToNil(frame.args["source"])
local pullRequestUrl = private.emptyToNil(frame.args["pull request"])
local links = private.parseCommaDelimited(frame.args["links"])
local warnings = private.parseCommaDelimited(frame.args["warnings"])
local devNote = private.emptyToNil(frame.args["dev note"])
local contentPackFor = private.emptyToNil(frame.args["content pack for"])
local mapLocalVersions = private.emptyToNil(frame.args["map local versions"])
local mapRemoteVersions = private.emptyToNil(frame.args["map remote versions"])
local changeUpdateKeys = private.emptyToNil(frame.args["change update keys"])
local betaSummary = private.emptyToNil(frame.args["beta summary"])
local betaBrokeIn = private.emptyToNil(frame.args["beta broke in"])
local betaStatus = private.emptyToNil(frame.args["beta status"])
local betaUnofficialVersion = private.emptyToNil(frame.args["beta unofficial version"])
local betaUnofficialUrl = private.emptyToNil(frame.args["beta unofficial url"])
-- get source url
local sourceUrl = customSource
if github then
sourceUrl = "https://github.com/" .. string.gsub(mw.uri.encode(github, "PATH"), "%%2F", "/")
end
local hasSource = sourceUrl ~= nil
-- parse compatibility
local compat = private.getCompatInfo(status, summary, brokeIn, unofficialVersion, unofficialUrl, hasSource)
local betaCompat = nil
if betaStatus or betaBrokeIn or betaUnofficialUrl or betaUnofficialVersion then
betaCompat = private.getCompatInfo(betaStatus, betaSummary, betaBrokeIn, betaUnofficialVersion, betaUnofficialUrl, hasSource)
end
-- get main URL
local url = nil
if nexusId then
url = "https://www.nexusmods.com/stardewvalley/mods/" .. mw.uri.encode(nexusId, "PATH")
elseif moddropId then
url = "https://www.moddrop.com/stardew-valley/mods/" .. mw.uri.encode(moddropId, "PATH")
elseif curseforgeId then
url = "https://www.curseforge.com/stardewvalley/mods/" .. mw.uri.encode(curseforgeId, "PATH")
elseif chucklefishId then
url = "https://community.playstarbound.com/resources/" .. mw.uri.encode(chucklefishId, "PATH")
else
url = customUrl
end
-- get background color
local background = '#999'
if compat.status == "ok" or compat.status == "optional" then
background = '#9F9'
elseif compat.status == "workaround" or compat.status == "unofficial" then
background = '#CF9'
elseif compat.status == "broken" then
background = '#F99'
elseif compat.status == "obsolete" or compat.status == "abandoned" then
background = '#999'
end
-- build HTML row
local row = mw.html.create("tr")
row:addClass("mod")
row:attr("id", names[1] and mw.uri.anchorEncode(names[1]));
row:attr("data-id", table.concat(ids, ","))
row:attr("data-name", table.concat(names, ","))
row:attr("data-author", table.concat(authors, ","))
row:attr("data-cf-id", chucklefishId)
row:attr("data-curseforge-id", curseforgeId)
row:attr("data-curseforge-key", curseforgeKey)
row:attr("data-moddrop-id", moddropId)
row:attr("data-nexus-id", nexusId)
row:attr("data-github", github)
row:attr("data-custom-source", customSource)
row:attr("data-url", url)
row:attr("data-status", compat.status)
row:attr("data-summary", compat.summary)
row:attr("data-broke-in", compat.brokeIn)
row:attr("data-unofficial-version", compat.unofficialVersion)
row:attr("data-unofficial-url", compat.unofficialUrl)
row:attr("data-beta-status", betaCompat and betaCompat.status)
row:attr("data-beta-summary", betaCompat and betaCompat.summary)
row:attr("data-beta-broke-in", betaCompat and betaCompat.brokeIn)
row:attr("data-beta-unofficial-version", betaCompat and betaCompat.unofficialVersion)
row:attr("data-beta-unofficial-url", betaCompat and betaCompat.unofficialUrl)
row:attr("data-pr", pullRequestUrl)
row:attr("data-warnings", private.emptyToNil(table.concat(warnings, ",")))
row:attr("data-content-pack-for", contentPackFor)
row:attr("data-dev-note", devNote)
row:attr("data-map-local-versions", mapLocalVersions)
row:attr("data-map-remote-versions", mapRemoteVersions)
row:attr("data-change-update-keys", changeUpdateKeys)
row:attr("style", "line-height: 1em; background: " .. background .. ";")
row:newline()
-- add name field
do
local field = mw.html.create("td")
field:wikitext("[" .. (url or '') .. " " .. (names[1] or '') .. "]")
local nameCount = #names
if nameCount > 1 then
field:wikitext("<br /><small>(aka ")
for i = 1, nameCount do
if i > 1 then
field:wikitext(names[i])
if i < nameCount then
field:wikitext(", ")
end
end
end
field:wikitext(")</small>")
end
row:node(field)
row:newline()
end
-- add author field
do
local field = mw.html.create("td")
field:wikitext(authors[1])
local authorCount = #authors
if authorCount > 1 then
field:wikitext("<br /><small>(aka ")
for i = 1, authorCount do
if i > 1 then
field:wikitext(authors[i])
if i < authorCount then
field:wikitext(", ")
end
end
end
field:wikitext(")</small>")
end
row:node(field)
row:newline()
end
-- add summary field
do
local field = mw.html.create("td")
-- stable status
field:wikitext(compat.summaryIcon .. " " .. compat.summary)
if compat.status == "optional" then
field:wikitext("<ref name=\"optional-update\" />")
end
-- beta status
if betaCompat ~= nil then
field:wikitext("<br />")
field:wikitext("'''SDV beta only:''' " .. betaCompat.summaryIcon .. " " .. betaCompat.summary)
if betaCompat.status == "optional" then
field:wikitext("<ref name=\"optional-update\" />")
end
end
-- warnings
do
local warningCount = #warnings
if warningCount > 0 then
for i = 1, warningCount do
field:wikitext("<br />⚠ " .. warnings[i])
end
end
end
row:node(field)
row:newline()
end
-- add 'broke in' field
do
local field = mw.html.create("td")
if betaCompat ~= nil and betaCompat.brokeIn ~= nil then
field:wikitext(betaCompat.brokeIn)
elseif compat.brokeIn ~= nil then
field:wikitext(compat.brokeIn)
end
row:node(field)
row:newline()
end
-- add 'source' field
do
if sourceUrl then
row:wikitext("<td>[" .. sourceUrl .. " source]</td>")
else
row:wikitext("<td><span style=\"color: red; font-size: 0.85em; opacity: 0.5;\">closed source</span></td>")
end
row:newline()
end
-- add metadata field
do
local field = mw.html.create("td")
field:attr("style", "font-size: 0.8em")
-- anchor
field:wikitext("[[#" .. (names[1] or '') .. "|#]] ")
-- pull request
if pullRequest then
field:wikitext("[" .. pullRequest .. " PR] ")
end
-- dev note
if devNote then
local devNoteField = mw.html.create("abbr")
devNoteField:attr("title", devNote)
devNoteField:wikitext("[dev note]")
field:node(devNoteField)
end
-- validation
if #ids == 0 then
field:wikitext("[⚠ no id] ")
end
if (not curseforgeId) ~= (not curseforgeKey) then
field:wikitext("<abbr title=\"The mod data is invalid: can't specify Curseforge key or ID without the other.\">[⚠ invalid data]</abbr>")
end
row:node(field)
end
return tostring(row)
end
-- Print an HTML attribute (like key="value") if a non-empty value is specified. This is a temporary method during the migration to Lua.
-- @param frame The arguments passed to the script.
-- @test mw.log(p.printAttribute({ args = { "someKey", "some \"'<> value" }}))
function p.printAttribute(frame)
local key = frame.args[1]
local value = frame.args[2]
if value == nil or value == "" then
return ""
else
return key .. "=\"" .. mw.text.encode(value) .. "\""
end
end
--##########
--## Private functions
--##########
-- Get the normalised compatibility info for a mod.
-- @param status The specified status code. If nil or blank, it'll be derived from the other fields.
-- @param summary A human-readable summary of the compatibility info. If nil or blank, it'll be derived from the other fields.
-- @param brokeIn The SMAPI or Stardew Valley version which broke the mod, if applicable.
-- @param unofficialVersion The unofficial version which fixes compatibility, if applicable.
-- @param unofficialUrl The URL for the unofficial version which fixes compatibility, if applicable.
-- @param hasSource Whether the mod has public source code available.
function private.getCompatInfo(status, summary, brokeIn, unofficialVersion, unofficialUrl, hasSource)
-- derive status
if status == nil then
if unofficialVersion ~= nil then
status = "unofficial"
elseif brokeIn ~= nil then
status = "broken"
else
status = "ok"
end
end
-- derive summary icon
local summaryIcon = "✓"
if status == "unofficial" or status == "workaround" then
summaryIcon = "⚠"
elseif status == "broken" and hasSource then
summaryIcon = "↻"
elseif status == "broken" or status == "obsolete" or status == "abandoned" then
summaryIcon = "✖"
end
-- derive summary
if not summary then
if status == "ok" then
summary = "use latest version."
elseif status == "optional" then
summary = "use optional download."
elseif status == "unofficial" then
summary = "broken, use [" .. (unofficialUrl or "") .. " " .. "unofficial version]"
if unofficialVersion ~= nil then
summary = summary .. " (<small>" .. unofficialVersion .. "</small>)"
end
summary = summary .. "."
elseif status == "workaround" then
summary = "broken. '''error:''' should specify summary."
elseif status == "broken" then
if hasSource then
summary = "broken, not updated yet."
else
summary = "broken, not open-source."
end
elseif status == "obsolete" then
summary = "remove this mod (obsolete)."
elseif status == "abandoned" then
summary = "remove this mod (no longer maintained)."
else
summary = "'''error:''' unknown status '" .. status .. "'."
end
end
return {
status = status,
summaryIcon = summaryIcon,
summary = summary,
brokeIn = brokeIn,
unofficialVersion = unofficialVersion,
unofficialUrl = unofficialUrl
}
end
-- Get a nil value if the specified value is an empty string, else return the value unchanged.
-- @param value The string to check.
function private.emptyToNil(value)
if value ~= "" then
return value
else
return nil
end
end
-- Parse a comma-delimited string into an array.
-- @param value The string to parse.
function private.parseCommaDelimited(value)
local result = {}
if value ~= nil then
local values = mw.text.split(value, ",", true)
for i = 1, #values do
v = mw.text.trim(values[i])
if v ~= "" then
table.insert(result, v)
end
end
end
return result
end
return p