Jump to content

Module:SMAPI compatibility: Difference between revisions

From Stardew Valley Wiki
imported>Pathoschild
add temporary printAttribute method
imported>Pathoschild
only output `data-url` attribute for custom mod page URLs to further reduce template include size
 
(32 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)
--## Constants
local enableBeta = false
--##########
-- The valid status values.
local statuses = {
  ok = "ok",
  optional = "optional",
  unofficial = "unofficial",
  workaround = "workaround",
  broken = "broken",
  abandoned = "abandoned",
  obsolete = "obsolete"
}
 


--##########
--##########
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)
   local row = mw.html.create("tr")
   return
  row:node(mw.html.create("th"):wikitext("mod name"))
    private.style(frame)
  row:node(mw.html.create("th"):wikitext("author"))
    .. '<table class="wikitable sortable plainlinks" id="mod-list">'
  row:node(mw.html.create("th"):wikitext("compatibility"))
    .. "<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>&nbsp;</th></tr>";
  row:node(mw.html.create("th"):wikitext("broke in"))
  row:node(mw.html.create("th"):wikitext("source"))
  row:node(mw.html.create("th"):wikitext("&nbsp;"))
 
  return '<table class="wikitable plainlinks">' .. tostring(row)
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>&nbsp;</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="Lookup Anything", name2="LookupAnything", author="Pathoschild", author2="Pathos", id="Pathoschild.LookupAnything", ["old ids"]="LookupAnything", ["nexus id"]="541", ["cf id"]="4250", ["github"]="Pathoschild/StardewMods", warnings="warning A, warning B", links="https://google.ca" }}))
-- @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((private.trim(frame.args["name"]) or '') .. ',' .. (private.trim(frame.args["name2"]) or ''))
   local names      = private.parseCommaDelimited(frame.args["name"] or '')
   local authors    = private.parseCommaDelimited((private.trim(frame.args["author"]) or '') .. ',' .. (private.trim(frame.args["author2"]) or ''))
   local authors    = private.parseCommaDelimited(frame.args["author"] or '')
   local ids        = private.parseCommaDelimited((private.trim(frame.args["id"]) or '') .. ',' .. (private.trim(frame.args["old ids"]) or ''))
   local ids        = private.parseCommaDelimited(frame.args["id"] or '')
   local nexusID   = private.emptyToNil(private.trim(frame.args["nexus id"]))
   local nexusId   = private.emptyToNil(frame.args["nexus"])
   local github    = private.emptyToNil(private.trim(frame.args["github"]))
   local github    = private.emptyToNil(frame.args["github"])
   local summary    = private.emptyToNil(private.trim(frame.args["summary"]))
   local summary    = private.emptyToNil(frame.args["summary"])
   local brokeIn    = private.emptyToNil(private.trim(frame.args["broke in"]))
   local brokeIn    = private.emptyToNil(frame.args["broke in"])
 
  local status    = private.emptyToNil(private.trim(frame.args["status"]))
  local unofficialVersion = private.emptyToNil(private.trim(frame.args["unofficial version"]))
  local unofficialUrl    = private.emptyToNil(private.trim(frame.args["unofficial url"]))
  local oldIDs        = private.emptyToNil(private.trim(frame.args["old ids"]))
  local chucklefishID = private.emptyToNil(private.trim(frame.args["cf id"]))
  local customUrl    = private.emptyToNil(private.trim(frame.args["url"]))
  local customSource  = private.emptyToNil(private.trim(frame.args["source"]))
  local name2        = private.emptyToNil(private.trim(frame.args["name2"]))
  local author2      = private.emptyToNil(private.trim(frame.args["author2"]))
  local links        = private.parseCommaDelimited(frame.args["links"])
  local warnings      = private.parseCommaDelimited(frame.args["warnings"])


   local betaSummary = private.emptyToNil(private.trim(frame.args["beta summary"]))
   local status    = private.emptyToNil(frame.args["status"])
   local betaBrokeIn = private.emptyToNil(private.trim(frame.args["beta broke in"]))
  local unofficialVersion = private.emptyToNil(frame.args["unofficial version"])
   local betaStatus = private.emptyToNil(private.trim(frame.args["beta status"]))
   local unofficialUrl    = private.emptyToNil(frame.args["unofficial url"])
   local betaUnofficialVersion = private.emptyToNil(private.trim(frame.args["beta unofficial version"]))
   local chucklefishId = private.emptyToNil(frame.args["chucklefish"])
   local betaUnofficialUrl    = private.emptyToNil(private.trim(frame.args["beta unofficial url"]))
  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"])


  -- parse compatibility
   local warnings      = private.parseCommaDelimited(frame.args["warnings"])
   local compat = private.getCompatInfo(status, summary, brokeIn, unofficialVersion, unofficialUrl)
   local devNote        = private.emptyToNil(frame.args["dev note"])
   local betaCompat = nil
   local contentPackFor = private.emptyToNil(frame.args["content pack for"])
   if betaStatus or betaBrokeIn or betaUnofficialUrl or betaUnofficialVersion then
    betaCompat = private.getCompatInfo(betaStatus, betaSummary, betaBrokeIn, betaUnofficialVersion, betaUnofficialUrl)
  end


   -- get main URL
   local betaSummary = nil
   local url = nil
  local betaBrokeIn = nil
   if nexusID then
   local betaStatus  = nil
     url = "https://www.nexusmods.com/stardewvalley/mods/" .. mw.uri.encode(nexusID, "PATH")
  local betaUnofficialVersion = nil
  elseif chucklefishID then
  local betaUnofficialUrl    = nil
     url = "https://community.playstarbound.com/resources/" .. mw.uri.encode(chucklefishID, "PATH")
   if enableBeta then
  else
     betaSummary = private.emptyToNil(frame.args["beta summary"])
     url = customUrl
    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 93: Line 98:
     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
   end
  local hasSource = sourceUrl ~= nil


   -- get background color
   -- parse compatibility
   local background = '#999'
  local compat = private.getCompatInfo(status, summary, brokeIn, unofficialVersion, unofficialUrl, hasSource)
   if compat.status == statuses.ok or compat.status == statuses.optional then
   local betaCompat = nil
     background = '#9F9'
   if enableBeta and (betaStatus or betaBrokeIn or betaUnofficialUrl or betaUnofficialVersion) then
   elseif compat.status == statuses.workaround or compat.status == statuses.unofficial then
    betaCompat = private.getCompatInfo(betaStatus, betaSummary, betaBrokeIn, betaUnofficialVersion, betaUnofficialUrl, hasSource)
     background = '#CF9'
  end
   elseif compat.status == statuses.broken then
 
     background = '#F99'
  -- get main URL
   elseif compat.status == statuses.obsolete or compat.status == statuses.abandoned then
  local isCustomUrl = false
     background = '#999'
  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
     isCustomUrl = true
   elseif hasSource then
    url = sourceUrl
     isCustomUrl = true
   end
   end


Line 109: Line 129:
   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]) or nil);
   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-id", table.concat(ids, ","))
   row:attr("data-author", table.concat(authors, ","))
   row:attr("data-author", table.concat(authors, ","))
   row:attr("data-url", url)
   row:attr("data-cf-id", chucklefishId)
   row:attr("data-nexus-id", nexusID)
  row:attr("data-curseforge-id", curseforgeId)
   row:attr("data-cf-id", chucklefishID)
   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)
  if isCustomUrl then
    row:attr("data-url", url)
  end
   row:attr("data-status", compat.status)
   row:attr("data-status", compat.status)
  row:attr("data-summary", compat.summary)
   row:attr("data-broke-in", compat.brokeIn)
   row:attr("data-broke-in", compat.brokeIn)
   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 or nil)
   if enableBeta then
  row:attr("data-beta-summary", betaCompat and betaCompat.summary or nil)
    row:attr("data-beta-status", betaCompat and betaCompat.status)
  row:attr("data-beta-broke-in", betaCompat and betaCompat.brokeIn or nil)
    row:attr("data-beta-broke-in", betaCompat and betaCompat.brokeIn)
  row:attr("data-beta-unofficial-version", betaCompat and betaCompat.unofficialVersion or nil)
    row:attr("data-beta-unofficial-version", betaCompat and betaCompat.unofficialVersion)
  row:attr("data-beta-unofficial-url", betaCompat and betaCompat.unofficialUrl or nil)
    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("style", "line-height: 1em; background: " .. background .. ";")
   row:attr("data-content-pack-for", contentPackFor)
  row:attr("data-dev-note", devNote)
   row:newline()
   row:newline()


Line 135: Line 160:
   do
   do
     local field = mw.html.create("td")
     local field = mw.html.create("td")
     field:wikitext("[" .. (url or '') .. " " .. (names[1] or '') .. "]")
     field:wikitext("[" .. (url or '') .. " " .. (names[1] or '') .. "]")


     if #names > 1 then
     local nameCount = #names
       local altNames = mw.html.create("small"):wikitext("(aka ")
    if nameCount > 1 then
       for k, v in pairs(names) do
       field:wikitext("<br /><small>(aka ")
         if k > 1 then
       for i = 1, nameCount do
           altNames:wikitext(v)
         if i > 1 then
           field:wikitext(names[i])
          if i < nameCount then
            field:wikitext(", ")
          end
         end
         end
       end
       end
       altNames:wikitext(")")
       field:wikitext(")</small>")
      field:node(mw.html.create("br"))
      field:node(altNames)
     end
     end


Line 158: Line 186:


     field:wikitext(authors[1])
     field:wikitext(authors[1])
     if #authors > 1 then
 
       local altNames = mw.html.create("small"):wikitext("(aka ")
     local authorCount = #authors
       for k, v in pairs(authors) do
    if authorCount > 1 then
         if k > 1 then
       field:wikitext("<br /><small>(aka ")
           altNames:wikitext(v)
       for i = 1, authorCount do
         if i > 1 then
           field:wikitext(authors[i])
          if i < authorCount then
            field:wikitext(", ")
          end
         end
         end
       end
       end
       altNames:wikitext(")")
       field:wikitext(")</small>")
      field:node(mw.html.create("br"))
      field:node(altNames)
     end
     end


Line 179: Line 210:


     -- stable status
     -- stable status
     field:wikitext(compat.summaryIcon .. " " .. compat.summary)
     field:wikitext("<span class=\"mod-summary\">" .. compat.summaryIcon .. " " .. compat.summary .. "</span>")
     if compat.status == statuses.optional then
     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 ~= nill then
     if betaCompat ~= nil then
       field:node(mw.html.create("br"))
       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 == statuses.optional then
       if betaCompat.status == "optional" then
         field:wikitext("<ref name=\"optional-update\" />")
         field:wikitext("<ref name=\"optional-update\" />")
       end
       end
Line 194: Line 225:


     -- warnings
     -- warnings
     if #warnings > 0 then
     do
      for k, v in pairs(warnings) do
      local warningCount = #warnings
        field:node(mw.html.create("br"))
      if warningCount > 0 then
        field:wikitext("⚠ " .. v)
        for i = 1, warningCount do
          field:wikitext("<br />⚠ " .. warnings[i])
        end
       end
       end
     end
     end
Line 221: Line 254:
   -- add 'source' field
   -- add 'source' field
   do
   do
    local field = mw.html.create("td")
     if sourceUrl then
     if sourceUrl then
       field:wikitext("[" .. sourceUrl .. " source]")
       row:wikitext("<td class=\"mod-source\">[" .. sourceUrl .. " source]</td>")
     else
     else
       field:wikitext("<span style=\"color: red; font-size: 0.85em; opacity: 0.5;\">closed source</span>")
       row:wikitext("<td class=\"mod-source\"><span>closed source</span></td>")
     end
     end
    row:node(field)
     row:newline()
     row:newline()
   end
   end
Line 236: Line 265:
   do
   do
     local field = mw.html.create("td")
     local field = mw.html.create("td")
     field:attr("style", "font-size: 0.8em")
     field:attr("class", "mod-metadata")


     -- anchor
     -- anchor
     field:wikitext("[[#" .. (names[1] or '') .. "|#]] ")
     field:wikitext("[[#" .. (names[1] or '') .. "|#]] ")


     -- reference links
     -- dev note
     for k, v in pairs(links) do
     if devNote then
       field:wikitext("[" .. v .. " " .. k .. "] ")
      local devNoteField = mw.html.create("abbr")
      devNoteField:attr("title", devNote)
       devNoteField:wikitext("[dev note]")
 
      field:node(devNoteField)
     end
     end


     -- 'no id' warning
     -- validation
     if #ids == 0 then
     if #ids == 0 then
       field:wikitext("⚠ no id")
       field:wikitext("[⚠ no id] ")
     end
     end


    -- backwards-compatible metadata (temporary)
    local metadata = mw.html.create("div")
    metadata:addClass("mod-metadata")
    metadata:attr("style", "display: none;")
    metadata:node(mw.html.create("div"):addClass("mod-anchor"):wikitext(names[1] and mw.uri.anchorEncode(names[1])))
    metadata:node(mw.html.create("div"):addClass("mod-id"):wikitext(table.concat(ids, ",")))
    metadata:node(mw.html.create("div"):addClass("mod-url"):wikitext(url))
    if nexusID ~= nil then
      metadata:node(mw.html.create("div"):addClass("mod-nexus-id"):wikitext(nexusID))
    end
    if chucklefishID ~= nil then
      metadata:node(mw.html.create("div"):addClass("mod-cf-id"):wikitext(chucklefishID))
    end
    if github ~= nil then
      metadata:node(mw.html.create("div"):addClass("mod-github"):wikitext(github))
    end
    if customSource ~= nil then
      metadata:node(mw.html.create("div"):addClass("mod-custom-source"):wikitext(customSource))
    end
    metadata:node(mw.html.create("div"):addClass("mod-status"):wikitext(compat.status))
    if compat.brokeIn ~= nil then
      metadata:node(mw.html.create("div"):addClass("mod-broke-in"):wikitext(compat.brokeIn))
    end
    if compat.unofficialVersion ~= nil and compat.unofficialUrl ~= nil then
      metadata:node(mw.html.create("div"):addClass("mod-unofficial-version"):wikitext(compat.unofficialVersion))
      metadata:node(mw.html.create("div"):addClass("mod-unofficial-url"):wikitext(compat.unofficialUrl))
    end
    if betaCompat ~= nil then
      metadata:node(mw.html.create("div"):addClass("mod-beta-status"):wikitext(betaCompat.status))
      if betaCompat.brokeIn ~= nil then
        metadata:node(mw.html.create("div"):addClass("mod-beta-broke-in"):wikitext(betaCompat.brokeIn))
      end
      if betaCompat.unofficialVersion ~= nil and betaCompat.unofficialUrl ~= nil then
        metadata:node(mw.html.create("div"):addClass("mod-beta-unofficial-version"):wikitext(betaCompat.unofficialVersion))
        metadata:node(mw.html.create("div"):addClass("mod-beta-unofficial-url"):wikitext(betaCompat.unofficialUrl))
      end
    end
    if #warnings > 0 then
      metadata:node(mw.html.create("div"):addClass("mod-warnings"):wikitext(table.concat(warnings, ",")))
    end
    field:node(metadata)
     row:node(field)
     row:node(field)
   end
   end
Line 299: Line 290:
end
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.
 
--##########
--## Private functions
--##########
-- Get the <templatestyles> tag for the module's stylesheet.
-- @param frame The arguments passed to the script.
-- @param frame The arguments passed to the script.
-- @test mw.log(p.printAttribute({ args = { key="someKey", value="some \"'<> value" }}))
function private.style(frame)
function p.printAttribute(frame)
   if frame.extensionTag ~= nil then
   local key = frame.args["key"]
     return frame:extensionTag('templatestyles', '', {src = 'Module:SMAPI compatibility/styles.css'})
  local value = frame.args["value"]
   else
 
     return "" -- called from the debug console
  if value == nil or value == "" then
     return ""
   else  
     return key .. "=\"" .. mw.text.encode(value) .. "\""
   end
   end
end
end


--##########
--## Private functions
--##########
-- Get the normalised compatibility info for a mod.
-- Get the normalised compatibility info for a mod.
-- @param status The specified status code (one of the `statuses` values). If nil or blank, it'll be derived from the other fields.
-- @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 325: Line 312:
-- @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)
  -- normalise values
  status = private.emptyToNil(private.trim(status))
  summary = private.emptyToNil(private.trim(summary))
  brokeIn = private.emptyToNil(private.trim(brokeIn))
  unofficialVersion = private.emptyToNil(private.trim(unofficialVersion))
  unofficialUrl = private.emptyToNil(private.trim(unofficialUrl))
   -- derive status
   -- derive status
   if status == nil then
   if status == nil then
     if unofficialVersion ~= nil then
     if unofficialVersion ~= nil then
       status = statuses.unofficial
       status = "unofficial"
     elseif brokeIn ~= nil then
     elseif brokeIn ~= nil then
       status = statuses.broken
       status = "broken"
     else
     else
       status = statuses.ok
       status = "ok"
     end
     end
   end
   end
Line 345: Line 325:
   -- derive summary icon
   -- derive summary icon
   local summaryIcon = "✓"
   local summaryIcon = "✓"
   if status == statuses.unofficial or status == statuses.workaround then
   if status == "unofficial" or status == "workaround" then
     summaryIcon = "⚠"
     summaryIcon = "⚠"
   elseif status == statuses.broken and hasSource then
   elseif status == "broken" and hasSource then
     summaryIcon = "↻"
     summaryIcon = "↻"
   elseif status == statuses.broken or status == statuses.obsolete or status == statuses.abandoned then
   elseif status == "broken" or status == "obsolete" or status == "abandoned" then
     summaryIcon = "✖"
     summaryIcon = "✖"
   end
   end


   -- derive summary
   -- derive summary
   if summary == nil then
   if not summary then
     if status == statuses.ok then
     if status == "ok" then
       summary = "use latest version."
       summary = "use latest version."
     elseif status == statuses.optional then
     elseif status == "optional" then
       summary = "use optional download."
       summary = "use optional download."
     elseif status == statuses.unofficial then
     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 365: Line 345:
       end
       end
       summary = summary .. "."
       summary = summary .. "."
     elseif status == statuses.workaround then
     elseif status == "workaround" then
       summary = "broken. '''error:''' should specify summary."
       summary = "broken. '''error:''' should specify summary."
     elseif status == statuses.broken then
     elseif status == "broken" then
       if hasSource then
       if hasSource then
         summary = "broken, not updated yet."
         summary = "broken, not updated yet."
Line 373: Line 353:
         summary = "broken, not open-source."
         summary = "broken, not open-source."
       end
       end
     elseif status == statuses.obsolete then
     elseif status == "obsolete" then
       summary = "obsolete."
       summary = "remove this mod (obsolete)."
     elseif status == statuses.abandoned then
     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 392: Line 372:
end
end


-- Trim a string value.
-- Call tostring() on the value if it's not nil, else return the value as-is.
-- @param value The string to trim.
-- @param value The value to format.
function private.trim(value)
function private.toSafeString(value)
   if value ~= nil then
   if value then
     return mw.text.trim(value)
     return tostring(value)
   else
   else
     return value
     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 check.
-- @param value The string to format.
function private.emptyToNil(value)
function private.emptyToNil(value)
   if value ~= "" then
   if value ~= "" then
Line 418: Line 398:


   if value ~= nil then
   if value ~= nil then
     for k, v in pairs(mw.text.split(value, ",", true)) do
     local values = mw.text.split(value, ",", true)
       v = mw.text.trim(v)
    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:10, 1 November 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
}}

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
}}

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

Usage

Limitations

The name, author, and id arguments are comma-separated. If the actual value contains a comma, use &#44; 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>&nbsp;</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>&nbsp;</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 isCustomUrl = false
  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
    isCustomUrl = true
  elseif hasSource then
    url = sourceUrl
    isCustomUrl = true
  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)
  if isCustomUrl then
    row:attr("data-url", url)
  end
  row:attr("data-status", compat.status)
  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-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