Difference between revisions of "Module:SMAPI compatibility"

From Stardew Valley Wiki
Jump to navigation Jump to search
(fix return value)
(add footer/header methods)
Line 20: Line 20:
 
--## Public functions
 
--## Public functions
 
--##########
 
--##########
 +
-- Start a SMAPI compatibility table.
 +
-- @test mw.log(p.header())
 +
function p.header()
 +
  local row = mw.html.create("tr")
 +
  row:node(mw.html.create("th"):wikitext("mod name"))
 +
  row:node(mw.html.create("th"):wikitext("author"))
 +
  row:node(mw.html.create("th"):wikitext("compatibility"))
 +
  row:node(mw.html.create("th"):wikitext("broke in"))
 +
  row:node(mw.html.create("th"):wikitext("source"))
 +
  row:node(mw.html.create("th"):wikitext(" "))
 +
 +
  return '<table class="wikitable plainlinks">' .. tostring(row)
 +
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.
 
--- 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.

Revision as of 01:30, 24 October 2018

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

Lua error at line 137: attempt to concatenate local 'url' (a nil value).

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

Lua error at line 137: attempt to concatenate local 'url' (a nil value).

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

Lua error at line 137: attempt to concatenate local 'url' (a nil value).

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

--##########
--## Constants
--##########
-- The valid status values.
local statuses = {
  ok = "ok",
  summary = "optional",
  unofficial = "unofficial",
  workaround = "workaround",
  broken = "broken",
  abandoned = "abandoned",
  obsolete = "obsolete"
}


--##########
--## Public functions
--##########
-- Start a SMAPI compatibility table.
-- @test mw.log(p.header())
function p.header()
  local row = mw.html.create("tr")
  row:node(mw.html.create("th"):wikitext("mod name"))
  row:node(mw.html.create("th"):wikitext("author"))
  row:node(mw.html.create("th"):wikitext("compatibility"))
  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 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", 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" }}))
function p.entry(frame)
  -- read input args
  local names      = private.parseCommaDelimited((private.trim(frame.args["name"]) or '') .. ',' .. (private.trim(frame.args["name2"]) or ''))
  local authors    = private.parseCommaDelimited((private.trim(frame.args["author"]) or '') .. ',' .. (private.trim(frame.args["author2"]) or ''))
  local ids        = private.parseCommaDelimited((private.trim(frame.args["id"]) or '') .. ',' .. (private.trim(frame.args["old ids"]) or ''))
  local nexusID    = private.emptyToNil(private.trim(frame.args["nexus id"]))
  local github     = private.emptyToNil(private.trim(frame.args["github"]))
  local summary    = private.emptyToNil(private.trim(frame.args["summary"]))
  local brokeIn    = private.emptyToNil(private.trim(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 betaBrokeIn = private.emptyToNil(private.trim(frame.args["beta broke in"]))
  local betaStatus  = private.emptyToNil(private.trim(frame.args["beta status"]))
  local betaUnofficialVersion = private.emptyToNil(private.trim(frame.args["beta unofficial version"]))
  local betaUnofficialUrl     = private.emptyToNil(private.trim(frame.args["beta unofficial url"]))

  -- parse compatibility
  local compat = private.getCompatInfo(status, summary, brokeIn, unofficialVersion, unofficialUrl)
  local betaCompat = nil
  if betaStatus or betaBrokeIn or betaUnofficialUrl or betaUnofficialVersion then
    betaCompat = private.getCompatInfo(betaStatus, betaSummary, betaBrokeIn, betaUnofficialVersion, betaUnofficialUrl)
  end

  -- get main URL
  local url = nil
  if nexusID then
    url = "https://www.nexusmods.com/stardewvalley/mods/" .. mw.uri.encode(nexusID, "PATH")
  elseif chucklefishID then
    url = "https://community.playstarbound.com/resources/" .. mw.uri.encode(chucklefishID, "PATH")
  else
    url = customUrl
  end

  -- get source url
  local sourceUrl = customSource
  if github then
    sourceUrl = "https://github.com/" .. string.gsub(mw.uri.encode(github, "PATH"), "%%2F", "/")
  end

  -- get background color
  local background = '#999'
  if status == statuses.ok or status == statuses.optional then
    background = '#9F9'
  elseif status == statuses.workaround or statuses.unofficial then
    background = '#CF9'
  elseif status == statuses.broken then
    background = '#F99'
  elseif status == statuses.obsolete or status == statuses.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]) or nil);
  row:attr("data-name", table.concat(names, ","))
  row:attr("data-id", table.concat(ids, ","))
  row:attr("data-author", table.concat(authors, ","))
  row:attr("data-url", url)
  row:attr("data-nexus-id", nexusID)
  row:attr("data-cf-id", chucklefishID)
  row:attr("data-github", github)
  row:attr("data-custom-source", customSource)
  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 or nil)
  row:attr("data-beta-summary", betaCompat and betaCompat.summary or nil)
  row:attr("data-beta-broke-in", betaCompat and betaCompat.brokeIn or nil)
  row:attr("data-beta-unofficial-version", betaCompat and betaCompat.unofficialVersion or nil)
  row:attr("data-beta-unofficial-url", betaCompat and betaCompat.unofficialUrl or nil)
  row:attr("data-warnings", private.emptyToNil(table.concat(warnings, ",")))
  row:attr("style", "line-height: 1em; background: " .. background .. ";")
  row:newline()

  -- add name field
  do
    local field = mw.html.create("td")
    field:wikitext("[" .. url .. " " .. names[1] .. "]")

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

    row:node(field)
    row:newline()
  end

  -- add author field
  do
    local field = mw.html.create("td")

    field:wikitext(authors[1])
    if #authors > 1 then
      local altNames = mw.html.create("small"):wikitext("(aka ")
      for k, v in pairs(authors) do
        if k > 1 then
          altNames:wikitext(v)
        end
      end
      altNames:wikitext(")")
      field:node(mw.html.create("br"))
      field:node(altNames)
    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 == statuses.optional then
      field:wikitext("<ref name=\"optional-update\" />")
    end

    -- beta status
    if betaCompat ~= nill then
      field:node(mw.html.create("br"))
      field:wikitext("'''SDV beta only:''' " .. betaCompat.summaryIcon .. " " .. betaCompat.summary)
      if betaCompat.status == statuses.optional then
        field:wikitext("<ref name=\"optional-update\" />")
      end
    end

    -- warnings
    if #warnings > 0 then
      for k, v in pairs(warnings) do
        field:node(mw.html.create("br"))
        field:wikitext("⚠ " .. v)
      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
    local field = mw.html.create("td")

    if sourceUrl then
      field:wikitext("[" .. sourceUrl .. " source]")
    else
      field:wikitext("<span style=\"color: red; font-size: 0.85em; opacity: 0.5;\">closed source</span>")
    end

    row:node(field)
    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] .. "|#]] ")

    -- reference links
    for k, v in pairs(links) do
      field:wikitext("[" .. v .. " " .. k .. "] ")
    end

    -- 'no id' warning
    if #ids == 0 then
      field:wikitext("⚠ no id")
    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]) or nil))
    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)
  end

  return tostring(row)
end


--##########
--## Private functions
--##########
-- 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 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)
  -- 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
  if status == nil then
    if unofficialVersion ~= nil then
      status = statuses.unofficial
    elseif brokeIn ~= nil then
      status = statuses.broken
    else
      status = statuses.ok
    end
  end

  -- derive summary icon
  local summaryIcon = "✓"
  if status == statuses.unofficial or status == statuses.workaround then
    summaryIcon = "⚠"
  elseif status == statuses.broken and hasSource then
    summaryIcon = "↻"
  elseif status == statuses.broken or status == statuses.obsolete or status == statuses.abandoned then
    summaryIcon = "✖"
  end

  -- derive summary
  if summary == nil then
    if status == statuses.ok then
      summary = "use latest version."
    elseif status == statuses.optional then
      summary = "use optional download."
    elseif status == statuses.unofficial then
      summary = "broken, use [" .. (unofficialUrl or "") .. " " .. "unofficial version]"
      if unofficialVersion ~= nil then
        summary = summary .. " (<small>" .. unofficialVersion .. "</small>)"
      end
      summary = summary .. "."
    elseif status == statuses.workaround then
      summary = "broken. '''error:''' should specify summary."
    elseif status == statuses.broken then
      if hasSource then
        summary = "broken, not updated yet."
      else
        summary = "broken, not open-source."
      end
    elseif status == statuses.obsolete then
      summary = "obsolete."
    elseif status == statuses.abandoned then
      summary = "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

-- Trim a string value.
-- @param value The string to trim.
function private.trim(value)
  if value ~= nil then
    return mw.text.trim(value)
  else
    return value
  end
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
    for k, v in pairs(mw.text.split(value, ",", true)) do
      v = mw.text.trim(v)
      if v ~= "" then
        table.insert(result, v)
      end
    end
  end

  return result
end

return p