SMAPI - Stardew Modding API

SMAPI - Stardew Modding API

971k Downloads

Extend mod data format

Pathoschild opened this issue ยท 3 comments

commented

Extend SMAPI's mod data format in StardewModdingAPI.config.json to support more flexible mod metadata (e.g. version-specific update keys).

commented

I propose a new format which...

  • moves ID resolution into a separate list and eliminates the custom format;
  • adds support for Name and Version overrides;
  • adds a Default prefix to only override a field if its value is empty;
  • versions all fields;
  • links to the compatibility page automatically if needed (instead of using alternative URLs);
  • and merges Compatibility and MapLocalVersions into versioned fields.

Here's some sample data in the current format:

"ModData": [
   // Hunger (Yyeadude)
   {
      "ID": "HungerYyeadude",
      "UpdateKeys": [ "Nexus:613" ]
   },

   // Access Chest Anywhere
   {
      "ID": "AccessChestAnywhere",
      "UpdateKeys": [ "Nexus:257" ],
      "AlternativeUrl": "https://stardewvalleywiki.com/Modding:SMAPI_2.0",
      "Compatibility": {
         "~1.1": { "Status": "AssumeBroken" } // broke in SDV 1.1
      },
      "MapLocalVersions": {
         "1.1-1078": "1.1"
      }
   },

   // More Rain
   {
      "ID": "{ID:'4108e859-333c-4fec-a1a7-d2e18c1019fe', Name:'More_Rain'} | Omegasis.MoreRain", // changed in 1.5; disambiguate from other mods by Alpha_Omegasis
      "UpdateKeys": [ "Nexus:441" ], // added in 1.5.1
      "Compatibility": {
         "~1.4": { "Status": "AssumeBroken" } // broke in SMAPI 2.0
      }
   },

   // Sprinkler Range
   {
      "ID": "cat.sprinklerrange",
      "UpdateKeys": [ "Nexus:1179" ],
      "MapRemoteVersions": {
         "1.0.1": "1.0" // manifest not updated
      }
   }
]

And here's the same data in the proposed format:

"MapIDs": [
   // More Rain
   // ID changed in 1.5; disambiguate from other mods with same ID
   {
      "From": {
         "ID": "4108e859-333c-4fec-a1a7-d2e18c1019fe",
         "Name": "More_Rain"
      }
      "To": "Omegasis.MoreRain"
   }
],
"ModData": [
   // Hunger (Yyeadude)
   {
      "ID": "HungerYyeadude"
      "~": {
         "DefaultUpdateKeys": [ "Nexus:613" ]
      }
   },

   // AccessChestAnywhere
   {
      "ID": "AccessChestAnywhere",
      "~": {
         "DefaultUpdateKeys": [ "Nexus:257" ]
      },
      "1.1-1078": {
         "Version": "1.1"
      },
      "~1.1": {
         "Status": "AssumeBroken" // broke in SDV 1.1
      }
   },

   // More Rain
   {
      "ID": "Omegasis.MoreRain",
      "~1.4": {
         "Status": "AssumeBroken" // broke in SMAPI 2.0
      },
      "~1.5": {
         "DefaultUpdateKeys": [ "Nexus:441" ] // added in 1.5.1
      }
   },

   // Sprinkler Range
   {
      "ID": "cat.sprinklerrange":,
      "MapRemoteVersions": [
         "1.0.1": "1.0" // manifest not updated
      }.
      "~":
         "DefaultUpdateKeys": [ "Nexus:1179" ]
      }
   }
]

While the new format is more verbose, it's also much more consistent. ID redirects now use simple JSON models instead of a custom format, all fields are set in the same way (notably the former MapLocalVersions and Compatibility fields), and ID resolution only happens once.

Notes:

  • This should be encapsulated so SMAPI can do things like this:
    ModData data = this.ModDatabase.GetModData(manifest);
    IManifest manifest = data.Apply(manifest);
    ISemanticVersion version = data.MapRemoteVersion("1.0.1");
  • The mod's manifest should include all resolved changes.
commented

I tried the new format a bit. I find it's too verbose and impacts readability and maintainability too much, given the number of records to maintain. We can also reduce the memory footprint by not parsing data into models until needed.

With that in mind, here's a proposed format that goes in the opposite direction. It's much more concise, even with more fields added. Notes:

  • DisplayName is the name SMAPI should use for this mod when the name is unavailable (e.g. because it's a missing dependency).
  • ID is the current ID.
  • FormerIDs matches previous IDs using the custom format currently in use (if any).
  • MapRemoteVersions is kept as-is.
  • All other fields have a special format.
    • The field name can be prefixed by any combination of version range and Default, separated by pipes (whitespace trimmed). For example, Name will always override the name, Default | Name will only override a blank name, and ~1.1 | Default | Name will override blank names up to version 1.1.
    • UpdateKey is a single value added to the UpdateKeys (to avoid needing arrays since there's always one value).

Here's the same data as before in the new format:

"ModData": [
   {
      "DisplayName": "Hunger (Yyeadude)",
      "ID": "HungerYyeadude",
      "Default | UpdateKey": "Nexus:613"
   },

   {
      "DisplayName": "AccessChestAnywhere",
      "ID": "AccessChestAnywhere",
      "Default  | UpdateKey": "Nexus:257",
      "1.1-1078 | Version": "1.1",
      "~1.1     | Status": "AssumeBroken"
   },

   {
      "DisplayName": "MoreRain",
      "ID": "Omegasis.MoreRain",
      "FormerIDs": "{ID:'4108e859-333c-4fec-a1a7-d2e18c1019fe', Name:'More_Rain'}", // ID changed in 1.5; disambiguate from other mods with same ID
      "Default | UpdateKey": "Nexus:441", // added in 1.5.1
      "~1.4    | Status": "AssumeBroken" // broke in SMAPI 2.0
   },

   {
      "DisplayName": "Sprinkler Range",
      "ID": "cat.sprinklerrange",
      "Default | UpdateKey": "Nexus:1179",
      "MapRemoteVersions": {
         "1.0.1": "1.0" // manifest not updated
      }
   }
]

This reduces each data record to one model:

public class ModEntry
{
   public string DisplayName { get; set; }
   public string ID { get; set; }
   public string FormerIDs { get; set; }
   public IDictionary<string, string> MapRemoteVersions { get; set; }
   public IDictionary<string, string> Fields { get; set; }
}

The data is further parsed only when needed. For example, SMAPI only needs to parse FormerIDs once when loading mods; afterwards it can be discarded. Likewise, the Fields only need to be parsed for records matching the loaded mods.

commented

Done in develop for the upcoming SMAPI 2.5. Here's the final data format:

"ModData": {
    "AccessChestAnywhere": {
      "ID": "AccessChestAnywhere",
      "MapLocalVersions": { "1.1-1078": "1.1" },
      "Default | UpdateKey": "Nexus:257",
      "~1.1    | Status": "AssumeBroken",
      "~1.1    | AlternativeUrl": "https://stardewvalleywiki.com/Modding:SMAPI_2.0"
    },

    "Hunger (Yyeadude)": {
      "ID": "HungerYyeadude",
      "Default | UpdateKey": "Nexus:613"
    },

    "More Rain": {
      "ID": "Omegasis.MoreRain",
      "FormerIDs": "{ID:'4108e859-333c-4fec-a1a7-d2e18c1019fe', Name:'More_Rain'}", // changed in 1.5; disambiguate from other mods by Alpha_Omegasis
      "Default | UpdateKey": "Nexus:441", // added in 1.5.1
      "~1.4    | Status": "AssumeBroken" // broke in SMAPI 2.0
    },

    "Sprinkler Range": {
      "ID": "cat.sprinklerrange",
      "MapRemoteVersions": { "1.0.1": "1.0" }, // manifest not updated
      "Default | UpdateKey": "Nexus:1179"
    }
}