Difference between revisions of "Module:SMAPI compatibility"
Pathoschild (talk | contribs) (fix status bugs) |
Pathoschild (talk | contribs) (disable beta fields (beta no longer in progress)) |
||
(33 intermediate revisions by 2 users not shown) | |||
Line 2: | Line 2: | ||
local private = {} | local private = {} | ||
− | -- | + | -- whether to handle Stardew Valley beta fields (don't forget to comment or uncomment the beta fields in /doc) |
− | + | local enableBeta = false | |
− | |||
− | |||
− | local | ||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
--########## | --########## | ||
Line 21: | Line 9: | ||
--########## | --########## | ||
-- Start a SMAPI compatibility table. | -- Start a SMAPI compatibility table. | ||
− | -- @test mw.log(p.header()) | + | -- @test mw.log(p.header({})) |
− | function p.header() | + | function p.header(frame) |
− | + | return | |
− | + | private.style(frame) | |
− | + | .. '<table class="wikitable sortable plainlinks" id="mod-list">' | |
− | + | .. "<tr><th>mod name</th><th>author</th><th><abbr title=\"This only shows whether a mod is *compatible*; it may have bugs unrelated to SMAPI compatibility.\">compatibility</abbr></th><th>broke in</th><th>source</th><th> </th></tr>"; | |
− | |||
− | |||
− | |||
− | |||
− | |||
end | end | ||
Line 38: | Line 21: | ||
function p.footer() | function p.footer() | ||
return '</table>' | return '</table>' | ||
+ | end | ||
+ | |||
+ | --- Render the SMAPI compatibility table based on JSON input. | ||
+ | -- @param frame The arguments passed to the script. | ||
+ | -- @test mw.log(p.table({ args = { [1]='[ { "name": "24h Clock", "author": "Lajna", // test\n"id": "Lajna.24hClock", "nexus": 1695, "github": "LajnaLegenden/Stardew_Valley_Mods", "brokeIn": "SMAPI 3.0", "unofficial": [ "1.0.1-unofficial.1-pathoschild", "https://community.playstarbound.com/threads/updating-mods-for-stardew-valley-1-3.142524/page-76#post-3342641" ] } ]' }})) | ||
+ | function p.table(frame) | ||
+ | -- parse data | ||
+ | local json = string.gsub(frame.args[1], '%s*//[^"\n]+', '') | ||
+ | local data = mw.text.jsonDecode(json, mw.text.JSON_TRY_FIXING) | ||
+ | |||
+ | -- start table | ||
+ | local table = mw.html.create("table") | ||
+ | table:addClass("wikitable sortable plainlinks") | ||
+ | table:attr("id", "mod-list") | ||
+ | table:wikitext("<tr><th>mod name</th><th>author</th><th><abbr title=\"This only shows whether a mod is *compatible*; it may have bugs unrelated to SMAPI compatibility.\">compatibility</abbr></th><th>broke in</th><th>source</th><th> </th></tr>") | ||
+ | |||
+ | -- add mod rows | ||
+ | for index,mod in pairs(data) do | ||
+ | -- temporarily passthrough args to avoid duplicating code until we migrate fully to JSON | ||
+ | -- (We need tostring on numeric fields since the previous code doesn't support numbers) | ||
+ | mod.chucklefish = private.toSafeString(mod.chucklefish) | ||
+ | mod.curse = private.toSafeString(mod.curse) | ||
+ | mod.moddrop = private.toSafeString(mod.moddrop) | ||
+ | mod.nexus = private.toSafeString(mod.nexus) | ||
+ | |||
+ | local row = p.entry({ args = mod }) | ||
+ | table:node(row) | ||
+ | end | ||
+ | |||
+ | -- return output | ||
+ | return private.style(frame) .. tostring(table) | ||
end | end | ||
--- Render a mod row in the SMAPI compatibility table. | --- Render a mod row in the SMAPI compatibility table. | ||
-- @param frame The arguments passed to the script. | -- @param frame The arguments passed to the script. | ||
− | -- @test mw.log(p.entry({ args = { name=" | + | -- @test mw.log(p.entry({ args = { name="Content Patcher, ContentPatcher", author="Pathoschild, Pathos", id="Pathoschild.ContentPatcher, ContentPatcher", nexus="1915", chucklefish="4250", curse="309243,content-patcher", github="Pathoschild/StardewMods", warnings="warning A, warning B" }})) |
function p.entry(frame) | function p.entry(frame) | ||
-- read input args | -- read input args | ||
− | local names = private.parseCommaDelimited | + | local names = private.parseCommaDelimited(frame.args["name"] or '') |
− | local authors = private.parseCommaDelimited | + | local authors = private.parseCommaDelimited(frame.args["author"] or '') |
− | local ids = private.parseCommaDelimited | + | local ids = private.parseCommaDelimited(frame.args["id"] or '') |
− | local | + | local nexusId = private.emptyToNil(frame.args["nexus"]) |
− | local github = private.emptyToNil | + | local github = private.emptyToNil(frame.args["github"]) |
− | local summary = private.emptyToNil | + | local summary = private.emptyToNil(frame.args["summary"]) |
− | local brokeIn = private.emptyToNil | + | local brokeIn = private.emptyToNil(frame.args["broke in"]) |
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | local | + | local status = private.emptyToNil(frame.args["status"]) |
− | local | + | local unofficialVersion = private.emptyToNil(frame.args["unofficial version"]) |
− | local | + | local unofficialUrl = private.emptyToNil(frame.args["unofficial url"]) |
− | local | + | local chucklefishId = private.emptyToNil(frame.args["chucklefish"]) |
− | local | + | local curseforgeId = private.emptyToNil(frame.args["curse"]) |
+ | local moddropId = private.emptyToNil(frame.args["moddrop"]) | ||
+ | local customUrl = private.emptyToNil(frame.args["url"]) | ||
+ | local customSource = private.emptyToNil(frame.args["source"]) | ||
− | + | local warnings = private.parseCommaDelimited(frame.args["warnings"]) | |
− | local | + | local devNote = private.emptyToNil(frame.args["dev note"]) |
− | local | + | local contentPackFor = private.emptyToNil(frame.args["content pack for"]) |
− | |||
− | |||
− | |||
− | + | local betaSummary = nil | |
− | local | + | local betaBrokeIn = nil |
− | if | + | local betaStatus = nil |
− | + | local betaUnofficialVersion = nil | |
− | + | local betaUnofficialUrl = nil | |
− | + | if enableBeta then | |
− | + | betaSummary = private.emptyToNil(frame.args["beta summary"]) | |
− | url | + | betaBrokeIn = private.emptyToNil(frame.args["beta broke in"]) |
+ | betaStatus = private.emptyToNil(frame.args["beta status"]) | ||
+ | betaUnofficialVersion = private.emptyToNil(frame.args["beta unofficial version"]) | ||
+ | betaUnofficialUrl = private.emptyToNil(frame.args["beta unofficial url"]) | ||
end | end | ||
Line 92: | Line 97: | ||
if github then | if github then | ||
sourceUrl = "https://github.com/" .. string.gsub(mw.uri.encode(github, "PATH"), "%%2F", "/") | 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 enableBeta and (betaStatus or betaBrokeIn or betaUnofficialUrl or betaUnofficialVersion) then | ||
+ | betaCompat = private.getCompatInfo(betaStatus, betaSummary, betaBrokeIn, betaUnofficialVersion, betaUnofficialUrl, hasSource) | ||
end | end | ||
− | -- get | + | -- get main URL |
− | local | + | local url = nil |
− | if | + | if nexusId then |
− | + | url = "https://www.nexusmods.com/stardewvalley/mods/" .. mw.uri.encode(nexusId, "PATH") | |
− | elseif | + | elseif moddropId then |
− | + | url = "https://www.moddrop.com/stardew-valley/mods/" .. mw.uri.encode(moddropId, "PATH") | |
− | elseif | + | elseif curseforgeId then |
− | + | url = "https://www.curseforge.com/projects/" .. mw.uri.encode(curseforgeId, "PATH") | |
− | elseif | + | elseif chucklefishId then |
− | + | url = "https://community.playstarbound.com/resources/" .. mw.uri.encode(chucklefishId, "PATH") | |
+ | elseif customUrl then | ||
+ | url = customUrl | ||
+ | elseif hasSource then | ||
+ | url = sourceUrl | ||
end | end | ||
Line 109: | Line 126: | ||
local row = mw.html.create("tr") | local row = mw.html.create("tr") | ||
row:addClass("mod") | row:addClass("mod") | ||
− | row:attr("id", names[1] and mw.uri.anchorEncode(names[1]) | + | 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-name", table.concat(names, ",")) | ||
− | |||
row:attr("data-author", table.concat(authors, ",")) | row:attr("data-author", table.concat(authors, ",")) | ||
− | row:attr("data- | + | row:attr("data-cf-id", chucklefishId) |
− | row:attr("data- | + | row:attr("data-curseforge-id", curseforgeId) |
− | row:attr("data- | + | row:attr("data-moddrop-id", moddropId) |
+ | row:attr("data-nexus-id", nexusId) | ||
row:attr("data-github", github) | row:attr("data-github", github) | ||
row:attr("data-custom-source", customSource) | row:attr("data-custom-source", customSource) | ||
+ | row:attr("data-url", url) | ||
row:attr("data-status", compat.status) | row:attr("data-status", compat.status) | ||
row:attr("data-summary", compat.summary) | row:attr("data-summary", compat.summary) | ||
Line 123: | Line 142: | ||
row:attr("data-unofficial-version", compat.unofficialVersion) | row:attr("data-unofficial-version", compat.unofficialVersion) | ||
row:attr("data-unofficial-url", compat.unofficialUrl) | row:attr("data-unofficial-url", compat.unofficialUrl) | ||
− | row:attr("data-beta-status", betaCompat and betaCompat.status | + | if enableBeta then |
− | + | 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) | ||
+ | end | ||
row:attr("data-warnings", private.emptyToNil(table.concat(warnings, ","))) | row:attr("data-warnings", private.emptyToNil(table.concat(warnings, ","))) | ||
− | row:attr(" | + | row:attr("data-content-pack-for", contentPackFor) |
+ | row:attr("data-dev-note", devNote) | ||
row:newline() | row:newline() | ||
Line 135: | Line 157: | ||
do | do | ||
local field = mw.html.create("td") | local field = mw.html.create("td") | ||
− | |||
− | + | field:wikitext("[" .. (url or '') .. " " .. (names[1] or '') .. "]") | |
− | + | ||
− | for | + | local nameCount = #names |
− | if | + | 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 | ||
end | end | ||
− | + | field:wikitext(")</small>") | |
− | |||
− | |||
end | end | ||
Line 158: | Line 183: | ||
field:wikitext(authors[1]) | field:wikitext(authors[1]) | ||
− | + | ||
− | + | local authorCount = #authors | |
− | for | + | if authorCount > 1 then |
− | if | + | 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 | ||
end | end | ||
− | + | field:wikitext(")</small>") | |
− | |||
− | |||
end | end | ||
Line 179: | Line 207: | ||
-- stable status | -- stable status | ||
− | field:wikitext(compat.summaryIcon .. " " .. compat.summary) | + | field:wikitext("<span class=\"mod-summary\">" .. compat.summaryIcon .. " " .. compat.summary .. "</span>") |
− | if compat.status == | + | if compat.status == "optional" then |
field:wikitext("<ref name=\"optional-update\" />") | field:wikitext("<ref name=\"optional-update\" />") | ||
end | end | ||
-- beta status | -- beta status | ||
− | if betaCompat ~= | + | if betaCompat ~= nil then |
− | field: | + | field:wikitext("<br />") |
− | field:wikitext("'''SDV beta only:''' " .. betaCompat.summaryIcon .. " " .. betaCompat.summary) | + | field:wikitext("'''SDV 1.6 beta only:''' <span class=\"mod-beta-summary\">" .. betaCompat.summaryIcon .. " " .. betaCompat.summary .. "</span>") |
− | if betaCompat.status == | + | if betaCompat.status == "optional" then |
field:wikitext("<ref name=\"optional-update\" />") | field:wikitext("<ref name=\"optional-update\" />") | ||
end | end | ||
Line 194: | Line 222: | ||
-- warnings | -- warnings | ||
− | + | do | |
− | + | local warningCount = #warnings | |
− | + | if warningCount > 0 then | |
− | + | for i = 1, warningCount do | |
+ | field:wikitext("<br />⚠ " .. warnings[i]) | ||
+ | end | ||
end | end | ||
end | end | ||
Line 221: | Line 251: | ||
-- add 'source' field | -- add 'source' field | ||
do | do | ||
− | |||
− | |||
if sourceUrl then | if sourceUrl then | ||
− | + | row:wikitext("<td class=\"mod-source\">[" .. sourceUrl .. " source]</td>") | |
else | else | ||
− | + | row:wikitext("<td class=\"mod-source\"><span>closed source</span></td>") | |
end | end | ||
− | |||
− | |||
row:newline() | row:newline() | ||
end | end | ||
Line 236: | Line 262: | ||
do | do | ||
local field = mw.html.create("td") | local field = mw.html.create("td") | ||
− | field:attr(" | + | field:attr("class", "mod-metadata") |
-- anchor | -- anchor | ||
− | field:wikitext("[[#" .. names[1] .. "|#]] ") | + | field:wikitext("[[#" .. (names[1] or '') .. "|#]] ") |
+ | |||
+ | -- dev note | ||
+ | if devNote then | ||
+ | local devNoteField = mw.html.create("abbr") | ||
+ | devNoteField:attr("title", devNote) | ||
+ | devNoteField:wikitext("[dev note]") | ||
− | + | field:node(devNoteField) | |
− | |||
− | field: | ||
end | end | ||
− | -- | + | -- validation |
if #ids == 0 then | if #ids == 0 then | ||
− | field:wikitext("⚠ no id") | + | field:wikitext("[⚠ no id] ") |
end | end | ||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
row:node(field) | row:node(field) | ||
end | end | ||
Line 303: | Line 291: | ||
--## Private functions | --## Private functions | ||
--########## | --########## | ||
+ | -- Get the <templatestyles> tag for the module's stylesheet. | ||
+ | -- @param frame The arguments passed to the script. | ||
+ | function private.style(frame) | ||
+ | if frame.extensionTag ~= nil then | ||
+ | return frame:extensionTag('templatestyles', '', {src = 'Module:SMAPI compatibility/styles.css'}) | ||
+ | else | ||
+ | return "" -- called from the debug console | ||
+ | end | ||
+ | end | ||
+ | |||
-- Get the normalised compatibility info for a mod. | -- Get the normalised compatibility info for a mod. | ||
− | -- @param status The specified status code | + | -- @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 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 brokeIn The SMAPI or Stardew Valley version which broke the mod, if applicable. | ||
Line 311: | Line 309: | ||
-- @param hasSource Whether the mod has public source code available. | -- @param hasSource Whether the mod has public source code available. | ||
function private.getCompatInfo(status, summary, brokeIn, unofficialVersion, unofficialUrl, hasSource) | function private.getCompatInfo(status, summary, brokeIn, unofficialVersion, unofficialUrl, hasSource) | ||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
-- derive status | -- derive status | ||
if status == nil then | if status == nil then | ||
if unofficialVersion ~= nil then | if unofficialVersion ~= nil then | ||
− | status = | + | status = "unofficial" |
elseif brokeIn ~= nil then | elseif brokeIn ~= nil then | ||
− | status = | + | status = "broken" |
else | else | ||
− | status = | + | status = "ok" |
end | end | ||
end | end | ||
Line 331: | Line 322: | ||
-- derive summary icon | -- derive summary icon | ||
local summaryIcon = "✓" | local summaryIcon = "✓" | ||
− | if status == | + | if status == "unofficial" or status == "workaround" then |
summaryIcon = "⚠" | summaryIcon = "⚠" | ||
− | elseif status == | + | elseif status == "broken" and hasSource then |
summaryIcon = "↻" | summaryIcon = "↻" | ||
− | elseif status == | + | elseif status == "broken" or status == "obsolete" or status == "abandoned" then |
summaryIcon = "✖" | summaryIcon = "✖" | ||
end | end | ||
-- derive summary | -- derive summary | ||
− | if summary | + | if not summary then |
− | if status == | + | if status == "ok" then |
summary = "use latest version." | summary = "use latest version." | ||
− | elseif status == | + | elseif status == "optional" then |
summary = "use optional download." | summary = "use optional download." | ||
− | elseif status == | + | elseif status == "unofficial" then |
summary = "broken, use [" .. (unofficialUrl or "") .. " " .. "unofficial version]" | summary = "broken, use [" .. (unofficialUrl or "") .. " " .. "unofficial version]" | ||
if unofficialVersion ~= nil then | if unofficialVersion ~= nil then | ||
Line 351: | Line 342: | ||
end | end | ||
summary = summary .. "." | summary = summary .. "." | ||
− | elseif status == | + | elseif status == "workaround" then |
summary = "broken. '''error:''' should specify summary." | summary = "broken. '''error:''' should specify summary." | ||
− | elseif status == | + | elseif status == "broken" then |
if hasSource then | if hasSource then | ||
summary = "broken, not updated yet." | summary = "broken, not updated yet." | ||
Line 359: | Line 350: | ||
summary = "broken, not open-source." | summary = "broken, not open-source." | ||
end | end | ||
− | elseif status == | + | elseif status == "obsolete" then |
− | summary = "obsolete." | + | summary = "remove this mod (obsolete)." |
− | elseif status == | + | elseif status == "abandoned" then |
− | summary = "no longer maintained." | + | summary = "remove this mod (no longer maintained)." |
else | else | ||
summary = "'''error:''' unknown status '" .. status .. "'." | summary = "'''error:''' unknown status '" .. status .. "'." | ||
Line 378: | Line 369: | ||
end | end | ||
− | -- | + | -- Call tostring() on the value if it's not nil, else return the value as-is. |
− | -- @param value The | + | -- @param value The value to format. |
− | function private. | + | function private.toSafeString(value) |
− | if value | + | if value then |
− | return | + | return tostring(value) |
else | else | ||
− | return | + | return nil |
end | end | ||
end | end | ||
-- Get a nil value if the specified value is an empty string, else return the value unchanged. | -- Get a nil value if the specified value is an empty string, else return the value unchanged. | ||
− | -- @param value The string to | + | -- @param value The string to format. |
function private.emptyToNil(value) | function private.emptyToNil(value) | ||
if value ~= "" then | if value ~= "" then | ||
Line 404: | Line 395: | ||
if value ~= nil then | if value ~= nil then | ||
− | + | local values = mw.text.split(value, ",", true) | |
− | v = mw.text.trim( | + | for i = 1, #values do |
+ | v = mw.text.trim(values[i]) | ||
if v ~= "" then | if v ~= "" then | ||
table.insert(result, v) | table.insert(result, v) |
Latest revision as of 15:12, 30 March 2024
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 = {}
-- whether to handle Stardew Valley beta fields (don't forget to comment or uncomment the beta fields in /doc)
local enableBeta = false
--##########
--## Public functions
--##########
-- Start a SMAPI compatibility table.
-- @test mw.log(p.header({}))
function p.header(frame)
return
private.style(frame)
.. '<table class="wikitable sortable plainlinks" id="mod-list">'
.. "<tr><th>mod name</th><th>author</th><th><abbr title=\"This only shows whether a mod is *compatible*; it may have bugs unrelated to SMAPI compatibility.\">compatibility</abbr></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 the SMAPI compatibility table based on JSON input.
-- @param frame The arguments passed to the script.
-- @test mw.log(p.table({ args = { [1]='[ { "name": "24h Clock", "author": "Lajna", // test\n"id": "Lajna.24hClock", "nexus": 1695, "github": "LajnaLegenden/Stardew_Valley_Mods", "brokeIn": "SMAPI 3.0", "unofficial": [ "1.0.1-unofficial.1-pathoschild", "https://community.playstarbound.com/threads/updating-mods-for-stardew-valley-1-3.142524/page-76#post-3342641" ] } ]' }}))
function p.table(frame)
-- parse data
local json = string.gsub(frame.args[1], '%s*//[^"\n]+', '')
local data = mw.text.jsonDecode(json, mw.text.JSON_TRY_FIXING)
-- start table
local table = mw.html.create("table")
table:addClass("wikitable sortable plainlinks")
table:attr("id", "mod-list")
table:wikitext("<tr><th>mod name</th><th>author</th><th><abbr title=\"This only shows whether a mod is *compatible*; it may have bugs unrelated to SMAPI compatibility.\">compatibility</abbr></th><th>broke in</th><th>source</th><th> </th></tr>")
-- add mod rows
for index,mod in pairs(data) do
-- temporarily passthrough args to avoid duplicating code until we migrate fully to JSON
-- (We need tostring on numeric fields since the previous code doesn't support numbers)
mod.chucklefish = private.toSafeString(mod.chucklefish)
mod.curse = private.toSafeString(mod.curse)
mod.moddrop = private.toSafeString(mod.moddrop)
mod.nexus = private.toSafeString(mod.nexus)
local row = p.entry({ args = mod })
table:node(row)
end
-- return output
return private.style(frame) .. tostring(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="Content Patcher, ContentPatcher", author="Pathoschild, Pathos", id="Pathoschild.ContentPatcher, ContentPatcher", nexus="1915", chucklefish="4250", curse="309243,content-patcher", github="Pathoschild/StardewMods", warnings="warning A, warning B" }}))
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"])
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["chucklefish"])
local curseforgeId = private.emptyToNil(frame.args["curse"])
local moddropId = private.emptyToNil(frame.args["moddrop"])
local customUrl = private.emptyToNil(frame.args["url"])
local customSource = private.emptyToNil(frame.args["source"])
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 betaSummary = nil
local betaBrokeIn = nil
local betaStatus = nil
local betaUnofficialVersion = nil
local betaUnofficialUrl = nil
if enableBeta then
betaSummary = private.emptyToNil(frame.args["beta summary"])
betaBrokeIn = private.emptyToNil(frame.args["beta broke in"])
betaStatus = private.emptyToNil(frame.args["beta status"])
betaUnofficialVersion = private.emptyToNil(frame.args["beta unofficial version"])
betaUnofficialUrl = private.emptyToNil(frame.args["beta unofficial url"])
end
-- 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 enableBeta and (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/projects/" .. mw.uri.encode(curseforgeId, "PATH")
elseif chucklefishId then
url = "https://community.playstarbound.com/resources/" .. mw.uri.encode(chucklefishId, "PATH")
elseif customUrl then
url = customUrl
elseif hasSource then
url = sourceUrl
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-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)
if enableBeta then
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)
end
row:attr("data-warnings", private.emptyToNil(table.concat(warnings, ",")))
row:attr("data-content-pack-for", contentPackFor)
row:attr("data-dev-note", devNote)
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("<span class=\"mod-summary\">" .. compat.summaryIcon .. " " .. compat.summary .. "</span>")
if compat.status == "optional" then
field:wikitext("<ref name=\"optional-update\" />")
end
-- beta status
if betaCompat ~= nil then
field:wikitext("<br />")
field:wikitext("'''SDV 1.6 beta only:''' <span class=\"mod-beta-summary\">" .. betaCompat.summaryIcon .. " " .. betaCompat.summary .. "</span>")
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 class=\"mod-source\">[" .. sourceUrl .. " source]</td>")
else
row:wikitext("<td class=\"mod-source\"><span>closed source</span></td>")
end
row:newline()
end
-- add metadata field
do
local field = mw.html.create("td")
field:attr("class", "mod-metadata")
-- anchor
field:wikitext("[[#" .. (names[1] or '') .. "|#]] ")
-- 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
row:node(field)
end
return tostring(row)
end
--##########
--## Private functions
--##########
-- Get the <templatestyles> tag for the module's stylesheet.
-- @param frame The arguments passed to the script.
function private.style(frame)
if frame.extensionTag ~= nil then
return frame:extensionTag('templatestyles', '', {src = 'Module:SMAPI compatibility/styles.css'})
else
return "" -- called from the debug console
end
end
-- 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
-- Call tostring() on the value if it's not nil, else return the value as-is.
-- @param value The value to format.
function private.toSafeString(value)
if value then
return tostring(value)
else
return nil
end
end
-- Get a nil value if the specified value is an empty string, else return the value unchanged.
-- @param value The string to format.
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