Extend mod data format
Pathoschild opened this issue ยท 3 comments
Extend SMAPI's mod data format in StardewModdingAPI.config.json
to support more flexible mod metadata (e.g. version-specific update keys).
I propose a new format which...
- moves ID resolution into a separate list and eliminates the custom format;
- adds support for
Name
andVersion
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
andMapLocalVersions
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.
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 theUpdateKeys
(to avoid needing arrays since there's always one value).
- The field name can be prefixed by any combination of version range and
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.
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"
}
}