summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/SMAPI.Mods.ConsoleCommands/manifest.json4
-rw-r--r--src/SMAPI.Mods.SaveBackup/ModEntry.cs60
-rw-r--r--src/SMAPI.Mods.SaveBackup/manifest.json4
-rw-r--r--src/SMAPI.Web/Startup.cs3
-rw-r--r--src/SMAPI.Web/ViewModels/ModModel.cs2
-rw-r--r--src/SMAPI.Web/Views/Mods/Index.cshtml19
-rw-r--r--src/SMAPI.Web/wwwroot/Content/js/mods.js178
-rw-r--r--src/SMAPI.Web/wwwroot/StardewModdingAPI.metadata.json117
-rw-r--r--src/SMAPI/Constants.cs90
-rw-r--r--src/SMAPI/Context.cs4
-rw-r--r--src/SMAPI/Framework/ContentCoordinator.cs2
-rw-r--r--src/SMAPI/Framework/DeprecationManager.cs27
-rw-r--r--src/SMAPI/Framework/DeprecationWarning.cs7
-rw-r--r--src/SMAPI/Framework/Input/SInputState.cs6
-rw-r--r--src/SMAPI/Framework/ModHelpers/ContentHelper.cs2
-rw-r--r--src/SMAPI/Framework/ModHelpers/DataHelper.cs2
-rw-r--r--src/SMAPI/Framework/ModLoading/ModResolver.cs15
-rw-r--r--src/SMAPI/Framework/ModRegistry.cs10
-rw-r--r--src/SMAPI/Framework/SCore.cs3
-rw-r--r--src/SMAPI/Framework/SGame.cs19
-rw-r--r--src/SMAPI/Metadata/CoreAssetPropagator.cs25
-rw-r--r--src/SMAPI/Metadata/InstructionMetadata.cs3
-rw-r--r--src/StardewModdingAPI.Toolkit/Framework/ModScanning/ModScanner.cs2
23 files changed, 418 insertions, 186 deletions
diff --git a/src/SMAPI.Mods.ConsoleCommands/manifest.json b/src/SMAPI.Mods.ConsoleCommands/manifest.json
index 0341c390..b5fd0424 100644
--- a/src/SMAPI.Mods.ConsoleCommands/manifest.json
+++ b/src/SMAPI.Mods.ConsoleCommands/manifest.json
@@ -1,9 +1,9 @@
{
"Name": "Console Commands",
"Author": "SMAPI",
- "Version": "2.10.1",
+ "Version": "2.10.2",
"Description": "Adds SMAPI console commands that let you manipulate the game.",
"UniqueID": "SMAPI.ConsoleCommands",
"EntryDll": "ConsoleCommands.dll",
- "MinimumApiVersion": "2.10.1"
+ "MinimumApiVersion": "2.10.2"
}
diff --git a/src/SMAPI.Mods.SaveBackup/ModEntry.cs b/src/SMAPI.Mods.SaveBackup/ModEntry.cs
index 56a86cd9..d10131b3 100644
--- a/src/SMAPI.Mods.SaveBackup/ModEntry.cs
+++ b/src/SMAPI.Mods.SaveBackup/ModEntry.cs
@@ -20,8 +20,11 @@ namespace StardewModdingAPI.Mods.SaveBackup
/// <summary>The absolute path to the folder in which to store save backups.</summary>
private readonly string BackupFolder = Path.Combine(Constants.ExecutionPath, "save-backups");
+ /// <summary>A unique label for the save backup to create.</summary>
+ private readonly string BackupLabel = $"{DateTime.UtcNow:yyyy-MM-dd} - SMAPI {Constants.ApiVersion} with Stardew Valley {Game1.version}";
+
/// <summary>The name of the save archive to create.</summary>
- private readonly string FileName = $"{DateTime.UtcNow:yyyy-MM-dd} - SMAPI {Constants.ApiVersion} with Stardew Valley {Game1.version}.zip";
+ private string FileName => $"{this.BackupLabel}.zip";
/*********
@@ -59,8 +62,9 @@ namespace StardewModdingAPI.Mods.SaveBackup
{
// get target path
FileInfo targetFile = new FileInfo(Path.Combine(backupFolder.FullName, this.FileName));
- if (targetFile.Exists)
- targetFile.Delete(); //return;
+ DirectoryInfo fallbackDir = new DirectoryInfo(Path.Combine(backupFolder.FullName, this.BackupLabel));
+ if (targetFile.Exists || fallbackDir.Exists)
+ return;
// create zip
// due to limitations with the bundled Mono on Mac, we can't reference System.IO.Compression.
@@ -70,12 +74,23 @@ namespace StardewModdingAPI.Mods.SaveBackup
case GamePlatform.Linux:
case GamePlatform.Windows:
{
- Assembly coreAssembly = Assembly.Load("System.IO.Compression, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089") ?? throw new InvalidOperationException("Can't load System.IO.Compression assembly.");
- Assembly fsAssembly = Assembly.Load("System.IO.Compression.FileSystem, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089") ?? throw new InvalidOperationException("Can't load System.IO.Compression assembly.");
- Type compressionLevelType = coreAssembly.GetType("System.IO.Compression.CompressionLevel") ?? throw new InvalidOperationException("Can't load CompressionLevel type.");
- Type zipFileType = fsAssembly.GetType("System.IO.Compression.ZipFile") ?? throw new InvalidOperationException("Can't load ZipFile type.");
- MethodInfo createMethod = zipFileType.GetMethod("CreateFromDirectory", new[] { typeof(string), typeof(string), compressionLevelType, typeof(bool) }) ?? throw new InvalidOperationException("Can't load ZipFile.CreateFromDirectory method.");
- createMethod.Invoke(null, new object[] { Constants.SavesPath, targetFile.FullName, CompressionLevel.Fastest, false });
+ try
+ {
+ // create compressed backup
+ Assembly coreAssembly = Assembly.Load("System.IO.Compression, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089") ?? throw new InvalidOperationException("Can't load System.IO.Compression assembly.");
+ Assembly fsAssembly = Assembly.Load("System.IO.Compression.FileSystem, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089") ?? throw new InvalidOperationException("Can't load System.IO.Compression assembly.");
+ Type compressionLevelType = coreAssembly.GetType("System.IO.Compression.CompressionLevel") ?? throw new InvalidOperationException("Can't load CompressionLevel type.");
+ Type zipFileType = fsAssembly.GetType("System.IO.Compression.ZipFile") ?? throw new InvalidOperationException("Can't load ZipFile type.");
+ MethodInfo createMethod = zipFileType.GetMethod("CreateFromDirectory", new[] { typeof(string), typeof(string), compressionLevelType, typeof(bool) }) ?? throw new InvalidOperationException("Can't load ZipFile.CreateFromDirectory method.");
+ createMethod.Invoke(null, new object[] { Constants.SavesPath, targetFile.FullName, CompressionLevel.Fastest, false });
+ }
+ catch (Exception ex) when (ex is TypeLoadException || ex.InnerException is TypeLoadException)
+ {
+ // create uncompressed backup if compression fails
+ this.Monitor.Log("Couldn't zip the save backup, creating uncompressed backup instead.");
+ this.Monitor.Log(ex.ToString(), LogLevel.Trace);
+ this.RecursiveCopy(new DirectoryInfo(Constants.SavesPath), fallbackDir, copyRoot: false);
+ }
}
break;
@@ -132,5 +147,32 @@ namespace StardewModdingAPI.Mods.SaveBackup
this.Monitor.Log(ex.ToString(), LogLevel.Trace);
}
}
+
+ /// <summary>Recursively copy a directory or file.</summary>
+ /// <param name="source">The file or folder to copy.</param>
+ /// <param name="targetFolder">The folder to copy into.</param>
+ /// <param name="copyRoot">Whether to copy the root folder itself, or <c>false</c> to only copy its contents.</param>
+ /// <remarks>Derived from the SMAPI installer code.</remarks>
+ private void RecursiveCopy(FileSystemInfo source, DirectoryInfo targetFolder, bool copyRoot = true)
+ {
+ if (!targetFolder.Exists)
+ targetFolder.Create();
+
+ switch (source)
+ {
+ case FileInfo sourceFile:
+ sourceFile.CopyTo(Path.Combine(targetFolder.FullName, sourceFile.Name));
+ break;
+
+ case DirectoryInfo sourceDir:
+ DirectoryInfo targetSubfolder = copyRoot ? new DirectoryInfo(Path.Combine(targetFolder.FullName, sourceDir.Name)) : targetFolder;
+ foreach (var entry in sourceDir.EnumerateFileSystemInfos())
+ this.RecursiveCopy(entry, targetSubfolder);
+ break;
+
+ default:
+ throw new NotSupportedException($"Unknown filesystem info type '{source.GetType().FullName}'.");
+ }
+ }
}
}
diff --git a/src/SMAPI.Mods.SaveBackup/manifest.json b/src/SMAPI.Mods.SaveBackup/manifest.json
index b2b9ad4b..7ac537ca 100644
--- a/src/SMAPI.Mods.SaveBackup/manifest.json
+++ b/src/SMAPI.Mods.SaveBackup/manifest.json
@@ -1,9 +1,9 @@
{
"Name": "Save Backup",
"Author": "SMAPI",
- "Version": "2.10.1",
+ "Version": "2.10.2",
"Description": "Automatically backs up all your saves once per day into its folder.",
"UniqueID": "SMAPI.SaveBackup",
"EntryDll": "SaveBackup.dll",
- "MinimumApiVersion": "2.10.1"
+ "MinimumApiVersion": "2.10.2"
}
diff --git a/src/SMAPI.Web/Startup.cs b/src/SMAPI.Web/Startup.cs
index 91553513..a2e47482 100644
--- a/src/SMAPI.Web/Startup.cs
+++ b/src/SMAPI.Web/Startup.cs
@@ -161,12 +161,13 @@ namespace StardewModdingAPI.Web
));
// shortcut redirects
+ redirects.Add(new RedirectToUrlRule(@"^/3\.0\.?$", "https://stardewvalleywiki.com/Modding:Migrate_to_SMAPI_3.0"));
redirects.Add(new RedirectToUrlRule(@"^/buildmsg(?:/?(.*))$", "https://github.com/Pathoschild/SMAPI/blob/develop/docs/mod-build-config.md#$1"));
redirects.Add(new RedirectToUrlRule(@"^/compat\.?$", "https://mods.smapi.io"));
- redirects.Add(new RedirectToUrlRule(@"^/3\.0\.?$", "https://stardewvalleywiki.com/Modding:Migrate_to_SMAPI_3.0"));
redirects.Add(new RedirectToUrlRule(@"^/docs\.?$", "https://stardewvalleywiki.com/Modding:Index"));
redirects.Add(new RedirectToUrlRule(@"^/install\.?$", "https://stardewvalleywiki.com/Modding:Player_Guide/Getting_Started#Install_SMAPI"));
redirects.Add(new RedirectToUrlRule(@"^/troubleshoot(.*)$", "https://stardewvalleywiki.com/Modding:Player_Guide/Troubleshooting$1"));
+ redirects.Add(new RedirectToUrlRule(@"^/xnb\.?$", "https://stardewvalleywiki.com/Modding:Using_XNB_mods"));
// redirect legacy canimod.com URLs
var wikiRedirects = new Dictionary<string, string[]>
diff --git a/src/SMAPI.Web/ViewModels/ModModel.cs b/src/SMAPI.Web/ViewModels/ModModel.cs
index f1a52f98..ae81acf5 100644
--- a/src/SMAPI.Web/ViewModels/ModModel.cs
+++ b/src/SMAPI.Web/ViewModels/ModModel.cs
@@ -107,7 +107,7 @@ namespace StardewModdingAPI.Web.ViewModels
if (entry.ModDropID.HasValue)
{
anyFound = true;
- yield return new ModLinkModel($"https://www.moddrop.com/sdv/mod/467243/{entry.ModDropID}", "ModDrop");
+ yield return new ModLinkModel($"https://www.moddrop.com/sdv/mod/{entry.ModDropID}", "ModDrop");
}
// fallback
diff --git a/src/SMAPI.Web/Views/Mods/Index.cshtml b/src/SMAPI.Web/Views/Mods/Index.cshtml
index a6c94cf1..a30a0048 100644
--- a/src/SMAPI.Web/Views/Mods/Index.cshtml
+++ b/src/SMAPI.Web/Views/Mods/Index.cshtml
@@ -4,15 +4,16 @@
ViewData["Title"] = "SMAPI mod compatibility";
}
@section Head {
- <link rel="stylesheet" href="~/Content/css/mods.css?r=20181122" />
+ <link rel="stylesheet" href="~/Content/css/mods.css?r=20190125" />
<script src="https://cdn.jsdelivr.net/npm/vue@2.5.17/dist/vue.min.js" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/jquery@3.3.1/dist/jquery.min.js" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/tablesorter@2.31.0/dist/js/jquery.tablesorter.combined.min.js" crossorigin="anonymous"></script>
- <script src="~/Content/js/mods.js?r=20181122"></script>
+ <script src="~/Content/js/mods.js?r=20190125"></script>
<script>
$(function() {
var data = @Json.Serialize(Model.Mods, new JsonSerializerSettings { Formatting = Formatting.None });
- smapi.modList(data);
+ var enableBeta = @Json.Serialize(Model.BetaVersion != null);
+ smapi.modList(data, enableBeta);
});
</script>
}
@@ -39,7 +40,7 @@
<label for="show-advanced">show advanced info and options</label>
<div id="filters" v-show="showAdvanced">
<div v-for="(filterGroup, key) in filters">
- {{key}}: <span v-for="filter in filterGroup" v-bind:class="{ active: filter.value }"><input type="checkbox" v-bind:id="filter.id" v-model="filter.value" v-on:change="applyFilters" /> <label v-bind:for="filter.id">{{filter.label}}</label></span>
+ {{filterGroup.label}}: <span v-for="filter in filterGroup.value" v-bind:class="{ active: filter.value }"><input type="checkbox" v-bind:id="filter.id" v-model="filter.value" v-on:change="applyFilters" /> <label v-bind:for="filter.id">{{filter.label}}</label></span>
</div>
</div>
</div>
@@ -93,12 +94,10 @@
<span v-else class="mod-closed-source">no source</span>
</td>
<td v-show="showAdvanced">
- <template v-if="mod.LatestCompatibility.Status == 'ok' || mod.LatestCompatibility.Status == 'unofficial' || mod.Smapi3Status == 'ok' || mod.Smapi3Status == 'soon'">
- <small v-if="mod.Smapi3Status == 'ok'">✓</small>
- <small v-else-if="mod.Smapi3Status == 'broken'">✖</small>
- <small v-else-if="mod.Smapi3Status == 'soon' && mod.Smapi3Url"><a v-bind:href="mod.Smapi3Url">↻ soon</a></small>
- <small v-else>↻ {{mod.Smapi3Status}}</small>
- </template>
+ <small v-if="mod.LatestCompatibility.Status == 'ok' || mod.LatestCompatibility.Status == 'unofficial' || mod.Smapi3Status == 'ok' || mod.Smapi3Status == 'soon' || mod.Smapi3Url">
+ <a v-if="mod.Smapi3Url" v-bind:href="mod.Smapi3Url">{{mod.Smapi3DisplayText}}</a>
+ <template v-else>{{mod.Smapi3DisplayText}}</template>
+ </small>
</td>
<td>
<small><a v-bind:href="'#' + mod.Slug">#</a></small>
diff --git a/src/SMAPI.Web/wwwroot/Content/js/mods.js b/src/SMAPI.Web/wwwroot/Content/js/mods.js
index 28992908..05114b00 100644
--- a/src/SMAPI.Web/wwwroot/Content/js/mods.js
+++ b/src/SMAPI.Web/wwwroot/Content/js/mods.js
@@ -2,7 +2,7 @@
var smapi = smapi || {};
var app;
-smapi.modList = function (mods) {
+smapi.modList = function (mods, enableBeta) {
// init data
var defaultStats = {
total: 0,
@@ -23,101 +23,73 @@ smapi.modList = function (mods) {
visibleStats: $.extend({}, defaultStats),
filters: {
source: {
- open: {
- label: "open",
- id: "show-open-source",
- value: true
- },
- closed: {
- label: "closed",
- id: "show-closed-source",
- value: true
+ value: {
+ open: { value: true },
+ closed: { value: true }
}
},
status: {
- ok: {
- label: "ok",
- id: "show-status-ok",
- value: true
- },
- optional: {
- label: "optional",
- id: "show-status-optional",
- value: true
- },
- unofficial: {
- label: "unofficial",
- id: "show-status-unofficial",
- value: true
- },
- workaround: {
- label: "workaround",
- id: "show-status-workaround",
- value: true
- },
- broken: {
- label: "broken",
- id: "show-status-broken",
- value: true
- },
- abandoned: {
- label: "abandoned",
- id: "show-status-abandoned",
- value: true
- },
- obsolete: {
- label: "obsolete",
- id: "show-status-obsolete",
- value: true
+ label: enableBeta ? "main status" : "status",
+ value: {
+ // note: keys must match status returned by the API
+ ok: { value: true },
+ optional: { value: true },
+ unofficial: { value: true },
+ workaround: { value: true },
+ broken: { value: true },
+ abandoned: { value: true },
+ obsolete: { value: true }
}
},
+ betaStatus: {
+ label: "beta status",
+ value: {} // cloned from status field if needed
+ },
download: {
- chucklefish: {
- label: "Chucklefish",
- id: "show-chucklefish",
- value: true
- },
- moddrop: {
- label: "ModDrop",
- id: "show-moddrop",
- value: true
- },
- nexus: {
- label: "Nexus",
- id: "show-nexus",
- value: true
- },
- custom: {
- label: "custom",
- id: "show-custom",
- value: true
+ value: {
+ chucklefish: { value: true, label: "Chucklefish" },
+ moddrop: { value: true, label: "ModDrop" },
+ nexus: { value: true, label: "Nexus" },
+ custom: { value: true }
}
},
- "SMAPI 3.0": {
- ok: {
- label: "ready",
- id: "show-smapi-3-ready",
- value: true
- },
- soon: {
- label: "soon",
- id: "show-smapi-3-soon",
- value: true
- },
- broken: {
- label: "broken",
- id: "show-smapi-3-broken",
- value: true
- },
- unknown: {
- label: "unknown",
- id: "show-smapi-3-unknown",
- value: true
+ smapi3: {
+ label: "SMAPI 3.0",
+ value: {
+ // note: keys must match status returned by the API
+ ok: { value: true, label: "ready" },
+ soon: { value: true },
+ broken: { value: true },
+ unknown: { value: true }
}
}
},
search: ""
};
+
+ // init filters
+ Object.entries(data.filters).forEach(([groupKey, filterGroup]) => {
+ filterGroup.label = filterGroup.label || groupKey;
+ Object.entries(filterGroup.value).forEach(([filterKey, filter]) => {
+ filter.id = ("filter_" + groupKey + "_" + filterKey).replace(/[^a-zA-Z0-9]/g, "_");
+ filter.label = filter.label || filterKey;
+ });
+ });
+
+ // init beta filters
+ if (enableBeta) {
+ var filterGroup = data.filters.betaStatus;
+ $.extend(true, filterGroup.value, data.filters.status.value);
+ Object.entries(filterGroup.value).forEach(([filterKey, filter]) => {
+ filter.id = "beta_" + filter.id;
+ });
+ }
+ else
+ delete data.filters.betaStatus;
+
+ window.boop = data.filters;
+
+ // init mods
for (var i = 0; i < data.mods.length; i++) {
var mod = mods[i];
@@ -127,6 +99,21 @@ smapi.modList = function (mods) {
// set overall compatibility
mod.LatestCompatibility = mod.BetaCompatibility || mod.Compatibility;
+ // set SMAPI 3.0 display text
+ switch (mod.Smapi3Status) {
+ case "ok":
+ mod.Smapi3DisplayText = "✓";
+ break;
+
+ case "broken":
+ mod.Smapi3DisplayText = "✖";
+ break;
+
+ default:
+ mod.Smapi3DisplayText = "↻ " + mod.Smapi3Status;
+ break;
+ }
+
// concatenate searchable text
mod.SearchableText = [mod.Name, mod.AlternateNames, mod.Author, mod.AlternateAuthors, mod.Compatibility.Summary, mod.BrokeIn];
if (mod.Compatibility.UnofficialVersion)
@@ -199,30 +186,37 @@ smapi.modList = function (mods) {
var filters = data.filters;
// check source
- if (!filters.source.open.value && mod.SourceUrl)
+ if (!filters.source.value.open.value && mod.SourceUrl)
return false;
- if (!filters.source.closed.value && !mod.SourceUrl)
+ if (!filters.source.value.closed.value && !mod.SourceUrl)
return false;
// check status
- var status = mod.LatestCompatibility.Status;
- if (filters.status[status] && !filters.status[status].value)
+ var mainStatus = mod.Compatibility.Status;
+ if (filters.status.value[mainStatus] && !filters.status.value[mainStatus].value)
return false;
+ // check beta status
+ if (enableBeta) {
+ var betaStatus = mod.LatestCompatibility.Status;
+ if (filters.betaStatus.value[betaStatus] && !filters.betaStatus.value[betaStatus].value)
+ return false;
+ }
+
// check SMAPI 3.0 compatibility
- if (filters["SMAPI 3.0"][mod.Smapi3Status] && !filters["SMAPI 3.0"][mod.Smapi3Status].value)
+ if (filters.smapi3.value[mod.Smapi3Status] && !filters.smapi3.value[mod.Smapi3Status].value)
return false;
// check download sites
var ignoreSites = [];
- if (!filters.download.chucklefish.value)
+ if (!filters.download.value.chucklefish.value)
ignoreSites.push("Chucklefish");
- if (!filters.download.moddrop.value)
+ if (!filters.download.value.moddrop.value)
ignoreSites.push("ModDrop");
- if (!filters.download.nexus.value)
+ if (!filters.download.value.nexus.value)
ignoreSites.push("Nexus");
- if (!filters.download.custom.value)
+ if (!filters.download.value.custom.value)
ignoreSites.push("custom");
if (ignoreSites.length) {
diff --git a/src/SMAPI.Web/wwwroot/StardewModdingAPI.metadata.json b/src/SMAPI.Web/wwwroot/StardewModdingAPI.metadata.json
index b16cb99f..45a4959d 100644
--- a/src/SMAPI.Web/wwwroot/StardewModdingAPI.metadata.json
+++ b/src/SMAPI.Web/wwwroot/StardewModdingAPI.metadata.json
@@ -52,6 +52,70 @@
*/
"ModData": {
/*********
+ ** Common dependencies for friendly errors
+ *********/
+ "Advanced Location Loader": {
+ "ID": "Entoarox.AdvancedLocationLoader",
+ "Default | UpdateKey": "Nexus:2270"
+ },
+
+ "Content Patcher": {
+ "ID": "Pathoschild.ContentPatcher",
+ "Default | UpdateKey": "Nexus:1915"
+ },
+
+ "Custom Farming Redux": {
+ "ID": "Platonymous.CustomFarming",
+ "Default | UpdateKey": "Nexus:991"
+ },
+
+ "Custom Shirts": {
+ "ID": "Platonymous.CustomShirts",
+ "Default | UpdateKey": "Nexus:2416"
+ },
+
+ "Entoarox Framework": {
+ "ID": "Entoarox.EntoaroxFramework",
+ "Default | UpdateKey": "Nexus:2269"
+ },
+
+ "JSON Assets": {
+ "ID": "spacechase0.JsonAssets",
+ "Default | UpdateKey": "Nexus:1720"
+ },
+
+ "Mail Framework": {
+ "ID": "DIGUS.MailFrameworkMod",
+ "Default | UpdateKey": "Nexus:1536"
+ },
+
+ "MTN": {
+ "ID": "SgtPickles.MTN",
+ "Default | UpdateKey": "Nexus:2256",
+ "~1.2.6 | Status": "AssumeBroken" // replaces Game1.multiplayer, which breaks SMAPI's multiplayer API.
+ },
+
+ "PyTK": {
+ "ID": "Platonymous.Toolkit",
+ "Default | UpdateKey": "Nexus:1726"
+ },
+
+ "SpaceCore": {
+ "ID": "spacechase0.SpaceCore",
+ "Default | UpdateKey": "Nexus:1348"
+ },
+
+ "Stardust Core": {
+ "ID": "Omegasis.StardustCore",
+ "Default | UpdateKey": "Nexus:2341"
+ },
+
+ "TMX Loader": {
+ "ID": "Platonymous.TMXLoader",
+ "Default | UpdateKey": "Nexus:1820"
+ },
+
+ /*********
** Content packs
*********/
"Canon-Friendly Dialogue Expansion": {
@@ -82,6 +146,11 @@
}
},
+ "Always Scroll Map": {
+ "ID": "bcmpinc.AlwaysScrollMap",
+ "~0.6 | Status": "AssumeBroken" // breaks newer versions of bcmpinc mods (per bcmpinc's request)
+ },
+
"Animal Mood Fix": {
"ID": "GPeters-AnimalMoodFix",
"~ | Status": "Obsolete",
@@ -151,6 +220,11 @@
"MapLocalVersions": { "1.2-beta": "1.2" }
},
+ "Craft Counter": {
+ "ID": "bcmpinc.CraftCounter",
+ "~0.6 | Status": "AssumeBroken" // breaks newer versions of bcmpinc mods (per bcmpinc's request)
+ },
+
"Crafting Counter": {
"ID": "lolpcgaming.CraftingCounter",
"MapRemoteVersions": { "1.1": "1.0" } // not updated in manifest
@@ -172,11 +246,6 @@
"MapRemoteVersions": { "1.1": "1.0" } // manifest not updated
},
- "Custom Shirts": {
- "ID": "Platonymous.CustomShirts",
- "Default | UpdateKey": "Nexus:2416" // keep for dependencies
- },
-
"Dynamic Horses": {
"ID": "Bpendragon-DynamicHorses",
"MapRemoteVersions": { "1.2": "1.1-release" } // manifest not updated
@@ -207,6 +276,11 @@
"~1.1 | Status": "AssumeBroken" // runtime errors with Harmony 1.2.0.1 in SMAPI 2.8+
},
+ "Fix Animal Tools": {
+ "ID": "bcmpinc.FixAnimalTools",
+ "~0.6 | Status": "AssumeBroken" // breaks newer versions of bcmpinc mods (per bcmpinc's request)
+ },
+
"Fix Scythe Exp": {
"ID": "bcmpinc.FixScytheExp",
"~0.3 | Status": "AssumeBroken" // broke in 1.3: Exception from HarmonyInstance "bcmpinc.FixScytheExp" [...] Bad label content in ILGenerator.
@@ -219,7 +293,7 @@
"Grass Growth": {
"ID": "bcmpinc.GrassGrowth",
- "~0.3 | Status": "AssumeBroken" // broke in 1.3.29 (runtime errors: System.IndexOutOfRangeException: Could not find instruction sequence)
+ "~0.6 | Status": "AssumeBroken" // breaks newer versions of bcmpinc mods (per bcmpinc's request)
},
"Hunger Mod (skn)": {
@@ -258,9 +332,9 @@
"~1.0.1 | Status": "AssumeBroken" // doesn't do anything as of SDV 1.2.33 (bad Harmony patch?)
},
- "MTN": {
- "ID": "SgtPickles.MTN",
- "~1.2.5 | Status": "AssumeBroken" // replaces Game1.multiplayer, which breaks SMAPI's multiplayer API.
+ "Movement Speed": {
+ "ID": "bcmpinc.MovementSpeed",
+ "~0.6 | Status": "AssumeBroken" // breaks newer versions of bcmpinc mods (per bcmpinc's request)
},
"Multiple Sprites and Portraits On Rotation (File Loading)": {
@@ -290,11 +364,6 @@
"Default | UpdateKey": "GitHub:lambui/StardewValleyMod_OmniFarm"
},
- "PyTK - Platonymous Toolkit": {
- "ID": "Platonymous.Toolkit",
- "Default | UpdateKey": "Nexus:1726"
- },
-
"Point-and-Plant": {
"ID": "jwdred.PointAndPlant",
"MapRemoteVersions": { "1.0.3": "1.0.2" } // manifest not updated
@@ -364,6 +433,11 @@
"~3.0.1 | Status": "AssumeBroken" // broke in SMAPI 2.6-beta.16 due to reflection into SMAPI internals
},
+ "Stardew Hack": {
+ "ID": "bcmpinc.StardewHack",
+ "~0.6 | Status": "AssumeBroken" // breaks newer versions of bcmpinc mods (per bcmpinc's request)
+ },
+
"Stardew Notification": {
"ID": "stardewnotification",
"Default | UpdateKey": "GitHub:monopandora/StardewNotification"
@@ -381,6 +455,11 @@
"~1.0.2 | Status": "AssumeBroken" // broke in SDV 1.3 (runtime errors)
},
+ "Tilled Soil Decay": {
+ "ID": "bcmpinc.TilledSoilDecay",
+ "~0.6 | Status": "AssumeBroken" // breaks newer versions of bcmpinc mods (per bcmpinc's request)
+ },
+
"Time Reminder": {
"ID": "KoihimeNakamura.TimeReminder",
"MapLocalVersions": { "1.0-20170314": "1.0.2" }
@@ -391,11 +470,21 @@
"Default | UpdateKey": "GitHub:mralbobo/stardew-tool-charging"
},
+ "Tree Spread": {
+ "ID": "bcmpinc.TreeSpread",
+ "~0.6 | Status": "AssumeBroken" // breaks newer versions of bcmpinc mods (per bcmpinc's request)
+ },
+
"Variable Grass": {
"ID": "dantheman999.VariableGrass",
"Default | UpdateKey": "GitHub:dantheman999301/StardewMods"
},
+ "Yet Another Harvest With Scythe Mod": {
+ "ID": "bcmpinc.HarvestWithScythe",
+ "~0.6 | Status": "AssumeBroken" // breaks newer versions of bcmpinc mods (per bcmpinc's request)
+ },
+
"Zoom Out Extreme": {
"ID": "RockinMods.ZoomMod",
"FormerIDs": "ZoomMod", // changed circa 1.2.1
diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs
index a0ba67ab..51c15269 100644
--- a/src/SMAPI/Constants.cs
+++ b/src/SMAPI/Constants.cs
@@ -2,6 +2,7 @@ using System;
using System.IO;
using System.Linq;
using System.Reflection;
+using StardewModdingAPI.Enums;
using StardewModdingAPI.Framework;
using StardewModdingAPI.Framework.ModLoading;
using StardewModdingAPI.Internal;
@@ -13,29 +14,19 @@ namespace StardewModdingAPI
public static class Constants
{
/*********
- ** Fields
- *********/
- /// <summary>The directory path containing the current save's data (if a save is loaded).</summary>
- private static string RawSavePath => Context.IsSaveLoaded ? Path.Combine(Constants.SavesPath, Constants.GetSaveFolderName()) : null;
-
- /// <summary>Whether the directory containing the current save's data exists on disk.</summary>
- private static bool SavePathReady => Context.IsSaveLoaded && Directory.Exists(Constants.RawSavePath);
-
-
- /*********
** Accessors
*********/
/****
** Public
****/
/// <summary>SMAPI's current semantic version.</summary>
- public static ISemanticVersion ApiVersion { get; } = new Toolkit.SemanticVersion("2.10.1");
+ public static ISemanticVersion ApiVersion { get; } = new Toolkit.SemanticVersion("2.10.2");
/// <summary>The minimum supported version of Stardew Valley.</summary>
public static ISemanticVersion MinimumGameVersion { get; } = new GameVersion("1.3.32");
/// <summary>The maximum supported version of Stardew Valley.</summary>
- public static ISemanticVersion MaximumGameVersion { get; } = null;
+ public static ISemanticVersion MaximumGameVersion { get; } = new GameVersion("1.3.33");
/// <summary>The target game platform.</summary>
public static GamePlatform TargetPlatform => (GamePlatform)Constants.Platform;
@@ -52,11 +43,33 @@ namespace StardewModdingAPI
/// <summary>The directory path where all saves are stored.</summary>
public static string SavesPath { get; } = Path.Combine(Constants.DataPath, "Saves");
- /// <summary>The directory name containing the current save's data (if a save is loaded and the directory exists).</summary>
- public static string SaveFolderName => Context.IsSaveLoaded ? Constants.GetSaveFolderName() : "";
+ /// <summary>The name of the current save folder (if save info is available, regardless of whether the save file exists yet).</summary>
+ public static string SaveFolderName
+ {
+ get
+ {
+ return Constants.GetSaveFolderName()
+#if SMAPI_3_0_STRICT
+ ;
+#else
+ ?? "";
+#endif
+ }
+ }
- /// <summary>The directory path containing the current save's data (if a save is loaded and the directory exists).</summary>
- public static string CurrentSavePath => Constants.SavePathReady ? Path.Combine(Constants.SavesPath, Constants.GetSaveFolderName()) : "";
+ /// <summary>The absolute path to the current save folder (if save info is available and the save file exists).</summary>
+ public static string CurrentSavePath
+ {
+ get
+ {
+ return Constants.GetSaveFolderPathIfExists()
+#if SMAPI_3_0_STRICT
+ ;
+#else
+ ?? "";
+#endif
+ }
+ }
/****
** Internal
@@ -184,13 +197,6 @@ namespace StardewModdingAPI
/*********
** Private methods
*********/
- /// <summary>Get the name of a save directory for the current player.</summary>
- private static string GetSaveFolderName()
- {
- string prefix = new string(Game1.player.Name.Where(char.IsLetterOrDigit).ToArray());
- return $"{prefix}_{Game1.uniqueIDForThisGame}";
- }
-
/// <summary>Get the game's current version string.</summary>
private static string GetGameVersion()
{
@@ -200,5 +206,43 @@ namespace StardewModdingAPI
throw new InvalidOperationException($"The {nameof(Game1)}.{nameof(Game1.version)} field could not be found.");
return (string)field.GetValue(null);
}
+
+ /// <summary>Get the name of the save folder, if any.</summary>
+ internal static string GetSaveFolderName()
+ {
+ // save not available
+ if (Context.LoadStage == LoadStage.None)
+ return null;
+
+ // get basic info
+ string playerName;
+ ulong saveID;
+ if (Context.LoadStage == LoadStage.SaveParsed)
+ {
+ playerName = SaveGame.loaded.player.Name;
+ saveID = SaveGame.loaded.uniqueIDForThisGame;
+ }
+ else
+ {
+ playerName = Game1.player.Name;
+ saveID = Game1.uniqueIDForThisGame;
+ }
+
+ // build folder name
+ return $"{new string(playerName.Where(char.IsLetterOrDigit).ToArray())}_{saveID}";
+ }
+
+ /// <summary>Get the path to the current save folder, if any.</summary>
+ internal static string GetSaveFolderPathIfExists()
+ {
+ string folderName = Constants.GetSaveFolderName();
+ if (folderName == null)
+ return null;
+
+ string path = Path.Combine(Constants.SavesPath, folderName);
+ return Directory.Exists(path)
+ ? path
+ : null;
+ }
}
}
diff --git a/src/SMAPI/Context.cs b/src/SMAPI/Context.cs
index cd1cf1c2..1cdef7f1 100644
--- a/src/SMAPI/Context.cs
+++ b/src/SMAPI/Context.cs
@@ -1,3 +1,4 @@
+using StardewModdingAPI.Enums;
using StardewModdingAPI.Events;
using StardewValley;
using StardewValley.Menus;
@@ -39,5 +40,8 @@ namespace StardewModdingAPI
/// <summary>Whether the game is currently writing to the save file.</summary>
internal static bool IsSaving => Game1.activeClickableMenu is SaveGameMenu || Game1.activeClickableMenu is ShippingMenu; // saving is performed by SaveGameMenu, but it's wrapped by ShippingMenu on days when the player shipping something
+
+ /// <summary>The current stage in the game's loading process.</summary>
+ internal static LoadStage LoadStage { get; set; }
}
}
diff --git a/src/SMAPI/Framework/ContentCoordinator.cs b/src/SMAPI/Framework/ContentCoordinator.cs
index 4dd1b6e1..ee654081 100644
--- a/src/SMAPI/Framework/ContentCoordinator.cs
+++ b/src/SMAPI/Framework/ContentCoordinator.cs
@@ -228,7 +228,7 @@ namespace StardewModdingAPI.Framework
{
IAssetInfo info = new AssetInfo(locale, assetName, type, this.MainContentManager.AssertAndNormaliseAssetName);
return predicate(info);
- });
+ }, dispose);
}
/// <summary>Purge matched assets from the cache.</summary>
diff --git a/src/SMAPI/Framework/DeprecationManager.cs b/src/SMAPI/Framework/DeprecationManager.cs
index 76c2f616..fcdf722e 100644
--- a/src/SMAPI/Framework/DeprecationManager.cs
+++ b/src/SMAPI/Framework/DeprecationManager.cs
@@ -62,7 +62,7 @@ namespace StardewModdingAPI.Framework
return;
// queue warning
- this.QueuedWarnings.Add(new DeprecationWarning(source, nounPhrase, version, severity));
+ this.QueuedWarnings.Add(new DeprecationWarning(source, nounPhrase, version, severity, Environment.StackTrace));
}
/// <summary>Print any queued messages.</summary>
@@ -78,27 +78,40 @@ namespace StardewModdingAPI.Framework
? $"{warning.ModName ?? "An unknown mod"} uses deprecated code (legacy events are deprecated since SMAPI {warning.Version})."
: $"{warning.ModName ?? "An unknown mod"} uses deprecated code ({warning.NounPhrase} is deprecated since SMAPI {warning.Version}).";
#endif
- if (warning.ModName == null)
- message += $"{Environment.NewLine}{Environment.StackTrace}";
- // log message
+ // get log level
+ LogLevel level;
switch (warning.Level)
{
case DeprecationLevel.Notice:
- this.Monitor.Log(message, LogLevel.Trace);
+ level = LogLevel.Trace;
break;
case DeprecationLevel.Info:
- this.Monitor.Log(message, LogLevel.Debug);
+ level = LogLevel.Debug;
break;
case DeprecationLevel.PendingRemoval:
- this.Monitor.Log(message, LogLevel.Warn);
+ level = LogLevel.Warn;
break;
default:
throw new NotSupportedException($"Unknown deprecation level '{warning.Level}'.");
}
+
+ // log message
+ if (warning.ModName != null)
+ this.Monitor.Log(message, level);
+ else
+ {
+ if (level == LogLevel.Trace)
+ this.Monitor.Log($"{message}\n{warning.StackTrace}", level);
+ else
+ {
+ this.Monitor.Log(message, level);
+ this.Monitor.Log(warning.StackTrace);
+ }
+ }
}
this.QueuedWarnings.Clear();
}
diff --git a/src/SMAPI/Framework/DeprecationWarning.cs b/src/SMAPI/Framework/DeprecationWarning.cs
index 25415012..5201b06c 100644
--- a/src/SMAPI/Framework/DeprecationWarning.cs
+++ b/src/SMAPI/Framework/DeprecationWarning.cs
@@ -18,6 +18,9 @@ namespace StardewModdingAPI.Framework
/// <summary>The deprecation level for the affected code.</summary>
public DeprecationLevel Level { get; }
+ /// <summary>The stack trace when the deprecation warning was raised.</summary>
+ public string StackTrace { get; }
+
/*********
** Public methods
@@ -27,12 +30,14 @@ namespace StardewModdingAPI.Framework
/// <param name="nounPhrase">A noun phrase describing what is deprecated.</param>
/// <param name="version">The SMAPI version which deprecated it.</param>
/// <param name="level">The deprecation level for the affected code.</param>
- public DeprecationWarning(string modName, string nounPhrase, string version, DeprecationLevel level)
+ /// <param name="stackTrace">The stack trace when the deprecation warning was raised.</param>
+ public DeprecationWarning(string modName, string nounPhrase, string version, DeprecationLevel level, string stackTrace)
{
this.ModName = modName;
this.NounPhrase = nounPhrase;
this.Version = version;
this.Level = level;
+ this.StackTrace = stackTrace;
}
}
}
diff --git a/src/SMAPI/Framework/Input/SInputState.cs b/src/SMAPI/Framework/Input/SInputState.cs
index 0228db0d..96a7003a 100644
--- a/src/SMAPI/Framework/Input/SInputState.cs
+++ b/src/SMAPI/Framework/Input/SInputState.cs
@@ -20,6 +20,9 @@ namespace StardewModdingAPI.Framework.Input
/// <summary>The cursor position on the screen adjusted for the zoom level.</summary>
private CursorPosition CursorPositionImpl;
+ /// <summary>The player's last known tile position.</summary>
+ private Vector2? LastPlayerTile;
+
/*********
** Accessors
@@ -83,13 +86,14 @@ namespace StardewModdingAPI.Framework.Input
MouseState realMouse = Mouse.GetState();
var activeButtons = this.DeriveStatuses(this.ActiveButtons, realKeyboard, realMouse, realController);
Vector2 cursorAbsolutePos = new Vector2(realMouse.X + Game1.viewport.X, realMouse.Y + Game1.viewport.Y);
+ Vector2? playerTilePos = Context.IsPlayerFree ? Game1.player.getTileLocation() : (Vector2?)null;
// update real states
this.ActiveButtons = activeButtons;
this.RealController = realController;
this.RealKeyboard = realKeyboard;
this.RealMouse = realMouse;
- if (this.CursorPositionImpl?.AbsolutePixels != cursorAbsolutePos)
+ if (cursorAbsolutePos != this.CursorPositionImpl?.AbsolutePixels || playerTilePos != this.LastPlayerTile)
this.CursorPositionImpl = this.GetCursorPosition(realMouse, cursorAbsolutePos);
// update suppressed states
diff --git a/src/SMAPI/Framework/ModHelpers/ContentHelper.cs b/src/SMAPI/Framework/ModHelpers/ContentHelper.cs
index dac627ba..7c353003 100644
--- a/src/SMAPI/Framework/ModHelpers/ContentHelper.cs
+++ b/src/SMAPI/Framework/ModHelpers/ContentHelper.cs
@@ -319,7 +319,7 @@ namespace StardewModdingAPI.Framework.ModHelpers
foreach (string candidateKey in new[] { imageSource, $@"Maps\{imageSource}" })
{
string contentKey = candidateKey.EndsWith(".png")
- ? candidateKey.Substring(0, imageSource.Length - 4)
+ ? candidateKey.Substring(0, candidateKey.Length - 4)
: candidateKey;
try
diff --git a/src/SMAPI/Framework/ModHelpers/DataHelper.cs b/src/SMAPI/Framework/ModHelpers/DataHelper.cs
index 2cb886ba..3b5c1752 100644
--- a/src/SMAPI/Framework/ModHelpers/DataHelper.cs
+++ b/src/SMAPI/Framework/ModHelpers/DataHelper.cs
@@ -97,7 +97,7 @@ namespace StardewModdingAPI.Framework.ModHelpers
if (!Game1.hasLoadedGame)
throw new InvalidOperationException($"Can't use {nameof(IMod.Helper)}.{nameof(IModHelper.Data)}.{nameof(this.WriteSaveData)} when a save file isn't loaded.");
if (!Game1.IsMasterGame)
- throw new InvalidOperationException($"Can't use {nameof(IMod.Helper)}.{nameof(IModHelper.Data)}.{nameof(this.ReadSaveData)} because this isn't the main player. (Save files are stored on the main player's computer.)");
+ throw new InvalidOperationException($"Can't use {nameof(IMod.Helper)}.{nameof(IModHelper.Data)}.{nameof(this.WriteSaveData)} because this isn't the main player. (Save files are stored on the main player's computer.)");
string internalKey = this.GetSaveFileKey(key);
if (data != null)
diff --git a/src/SMAPI/Framework/ModLoading/ModResolver.cs b/src/SMAPI/Framework/ModLoading/ModResolver.cs
index 835b0a54..a8564524 100644
--- a/src/SMAPI/Framework/ModLoading/ModResolver.cs
+++ b/src/SMAPI/Framework/ModLoading/ModResolver.cs
@@ -137,12 +137,23 @@ namespace StardewModdingAPI.Framework.ModLoading
}
// invalid path
- string assemblyPath = Path.Combine(mod.DirectoryPath, mod.Manifest.EntryDll);
- if (!File.Exists(assemblyPath))
+ if (!File.Exists(Path.Combine(mod.DirectoryPath, mod.Manifest.EntryDll)))
{
mod.SetStatus(ModMetadataStatus.Failed, $"its DLL '{mod.Manifest.EntryDll}' doesn't exist.");
continue;
}
+
+ // invalid capitalisation
+ string actualFilename = new DirectoryInfo(mod.DirectoryPath).GetFiles(mod.Manifest.EntryDll).FirstOrDefault()?.Name;
+ if (actualFilename != mod.Manifest.EntryDll)
+ {
+#if SMAPI_3_0_STRICT
+ mod.SetStatus(ModMetadataStatus.Failed, $"its {nameof(IManifest.EntryDll)} value '{mod.Manifest.EntryDll}' doesn't match the actual file capitalisation '{actualFilename}'. The capitalisation must match for crossplatform compatibility.");
+ continue;
+#else
+ SCore.DeprecationManager.Warn(mod.DisplayName, $"{nameof(IManifest.EntryDll)} value with case-insensitive capitalisation", "2.11", DeprecationLevel.Info);
+#endif
+ }
}
// validate content pack
diff --git a/src/SMAPI/Framework/ModRegistry.cs b/src/SMAPI/Framework/ModRegistry.cs
index e9ceb66e..5be33cb4 100644
--- a/src/SMAPI/Framework/ModRegistry.cs
+++ b/src/SMAPI/Framework/ModRegistry.cs
@@ -33,8 +33,14 @@ namespace StardewModdingAPI.Framework
public void Add(IModMetadata metadata)
{
this.Mods.Add(metadata);
- if (!metadata.IsContentPack)
- this.ModNamesByAssembly[metadata.Mod.GetType().Assembly.FullName] = metadata;
+ }
+
+ /// <summary>Track a mod's assembly for use via <see cref="GetFrom"/>.</summary>
+ /// <param name="metadata">The mod metadata.</param>
+ /// <param name="modAssembly">The mod assembly.</param>
+ public void TrackAssemblies(IModMetadata metadata, Assembly modAssembly)
+ {
+ this.ModNamesByAssembly[modAssembly.FullName] = metadata;
}
/// <summary>Get metadata for all loaded mods.</summary>
diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs
index 27c0c40b..ec3e9f72 100644
--- a/src/SMAPI/Framework/SCore.cs
+++ b/src/SMAPI/Framework/SCore.cs
@@ -977,11 +977,12 @@ namespace StardewModdingAPI.Framework
try
{
modAssembly = assemblyLoader.Load(mod, assemblyPath, assumeCompatible: mod.DataRecord?.Status == ModStatus.AssumeCompatible);
+ this.ModRegistry.TrackAssemblies(mod, modAssembly);
}
catch (IncompatibleInstructionException) // details already in trace logs
{
string[] updateUrls = new[] { modDatabase.GetModPageUrlFor(manifest.UniqueID), "https://mods.smapi.io" }.Where(p => p != null).ToArray();
- errorReasonPhrase = $"it's no longer compatible. Please check for a new version at {string.Join(" or ", updateUrls)}.";
+ errorReasonPhrase = $"it's no longer compatible. Please check for a new version at {string.Join(" or ", updateUrls)}";
return false;
}
catch (SAssemblyLoadFailedException ex)
diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs
index 25ffcabd..9818314a 100644
--- a/src/SMAPI/Framework/SGame.cs
+++ b/src/SMAPI/Framework/SGame.cs
@@ -69,9 +69,6 @@ namespace StardewModdingAPI.Framework
/// <remarks>Skipping a few frames ensures the game finishes initialising the world before mods try to change it.</remarks>
private readonly Countdown AfterLoadTimer = new Countdown(5);
- /// <summary>The current stage in the game's loading process.</summary>
- private LoadStage LoadStage = LoadStage.None;
-
/// <summary>Whether the game is saving and SMAPI has already raised <see cref="IGameLoopEvents.Saving"/>.</summary>
private bool IsBetweenSaveEvents;
@@ -215,12 +212,12 @@ namespace StardewModdingAPI.Framework
internal void OnLoadStageChanged(LoadStage newStage)
{
// nothing to do
- if (newStage == this.LoadStage)
+ if (newStage == Context.LoadStage)
return;
// update data
- LoadStage oldStage = this.LoadStage;
- this.LoadStage = newStage;
+ LoadStage oldStage = Context.LoadStage;
+ Context.LoadStage = newStage;
if (newStage == LoadStage.None)
{
this.Monitor.Log("Context: returned to title", LogLevel.Trace);
@@ -293,6 +290,7 @@ namespace StardewModdingAPI.Framework
// Run async tasks synchronously to avoid issues due to mod events triggering
// concurrently with game code.
+ bool saveParsed = false;
if (Game1.currentLoader != null)
{
this.Monitor.Log("Game loader synchronising...", LogLevel.Trace);
@@ -301,7 +299,8 @@ namespace StardewModdingAPI.Framework
// raise load stage changed
switch (Game1.currentLoader.Current)
{
- case 20:
+ case 20 when (!saveParsed && SaveGame.loaded != null):
+ saveParsed = true;
this.OnLoadStageChanged(LoadStage.SaveParsed);
break;
@@ -511,10 +510,10 @@ namespace StardewModdingAPI.Framework
*********/
if (wasWorldReady && !Context.IsWorldReady)
this.OnLoadStageChanged(LoadStage.None);
- else if (Context.IsWorldReady && this.LoadStage != LoadStage.Ready)
+ else if (Context.IsWorldReady && Context.LoadStage != LoadStage.Ready)
{
// print context
- string context = $"Context: loaded saved game '{Constants.SaveFolderName}', starting {Game1.currentSeason} {Game1.dayOfMonth} Y{Game1.year}.";
+ string context = $"Context: loaded save '{Constants.SaveFolderName}', starting {Game1.currentSeason} {Game1.dayOfMonth} Y{Game1.year}, locale set to {this.ContentCore.Language}.";
if (Context.IsMultiplayer)
{
int onlineCount = Game1.getOnlineFarmers().Count();
@@ -884,7 +883,7 @@ namespace StardewModdingAPI.Framework
events.GameLaunched.Raise(new GameLaunchedEventArgs());
// preloaded
- if (Context.IsSaveLoaded && this.LoadStage != LoadStage.Loaded && this.LoadStage != LoadStage.Ready)
+ if (Context.IsSaveLoaded && Context.LoadStage != LoadStage.Loaded && Context.LoadStage != LoadStage.Ready)
this.OnLoadStageChanged(LoadStage.Loaded);
// update tick
diff --git a/src/SMAPI/Metadata/CoreAssetPropagator.cs b/src/SMAPI/Metadata/CoreAssetPropagator.cs
index d83fc748..a64dc89b 100644
--- a/src/SMAPI/Metadata/CoreAssetPropagator.cs
+++ b/src/SMAPI/Metadata/CoreAssetPropagator.cs
@@ -2,7 +2,7 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
-using Microsoft.Xna.Framework;
+using System.Reflection;
using Microsoft.Xna.Framework.Graphics;
using StardewModdingAPI.Framework.Reflection;
using StardewValley;
@@ -99,8 +99,21 @@ namespace StardewModdingAPI.Metadata
{
if (!string.IsNullOrWhiteSpace(location.mapPath.Value) && this.GetNormalisedPath(location.mapPath.Value) == key)
{
+ // reload map data
this.Reflection.GetMethod(location, "reloadMap").Invoke();
this.Reflection.GetMethod(location, "updateWarps").Invoke();
+
+ // reload doors
+ {
+ Type interiorDoorDictType = Type.GetType($"StardewValley.InteriorDoorDictionary, {Constants.GameAssemblyName}", throwOnError: true);
+ ConstructorInfo constructor = interiorDoorDictType.GetConstructor(new[] { typeof(GameLocation) });
+ if (constructor == null)
+ throw new InvalidOperationException("Can't reset location doors: constructor not found for InteriorDoorDictionary type.");
+ object instance = constructor.Invoke(new object[] { location });
+
+ this.Reflection.GetField<object>(location, "interiorDoors").SetValue(instance);
+ }
+
anyChanged = true;
}
}
@@ -529,7 +542,7 @@ namespace StardewModdingAPI.Metadata
{
// get NPCs
NPC[] characters = this.GetCharacters()
- .Where(npc => this.GetNormalisedPath(npc.Sprite.textureName.Value) == key)
+ .Where(npc => npc.Sprite != null && this.GetNormalisedPath(npc.Sprite.textureName.Value) == key)
.ToArray();
if (!characters.Any())
return false;
@@ -677,7 +690,13 @@ namespace StardewModdingAPI.Metadata
/// <summary>Get all locations in the game.</summary>
private IEnumerable<GameLocation> GetLocations()
{
- foreach (GameLocation location in Game1.locations)
+ // get available root locations
+ IEnumerable<GameLocation> rootLocations = Game1.locations;
+ if (SaveGame.loaded?.locations != null)
+ rootLocations = rootLocations.Concat(SaveGame.loaded.locations);
+
+ // yield root + child locations
+ foreach (GameLocation location in rootLocations)
{
yield return location;
diff --git a/src/SMAPI/Metadata/InstructionMetadata.cs b/src/SMAPI/Metadata/InstructionMetadata.cs
index 9ff99440..272ceb09 100644
--- a/src/SMAPI/Metadata/InstructionMetadata.cs
+++ b/src/SMAPI/Metadata/InstructionMetadata.cs
@@ -51,7 +51,8 @@ namespace StardewModdingAPI.Metadata
yield return new FieldFinder(typeof(SaveGame).FullName, nameof(SaveGame.serializer), InstructionHandleResult.DetectedSaveSerialiser);
yield return new FieldFinder(typeof(SaveGame).FullName, nameof(SaveGame.farmerSerializer), InstructionHandleResult.DetectedSaveSerialiser);
yield return new FieldFinder(typeof(SaveGame).FullName, nameof(SaveGame.locationSerializer), InstructionHandleResult.DetectedSaveSerialiser);
- yield return new TypeFinder(typeof(ISpecialisedEvents).FullName, InstructionHandleResult.DetectedUnvalidatedUpdateTick);
+ yield return new EventFinder(typeof(ISpecialisedEvents).FullName, nameof(ISpecialisedEvents.UnvalidatedUpdateTicked), InstructionHandleResult.DetectedUnvalidatedUpdateTick);
+ yield return new EventFinder(typeof(ISpecialisedEvents).FullName, nameof(ISpecialisedEvents.UnvalidatedUpdateTicking), InstructionHandleResult.DetectedUnvalidatedUpdateTick);
#if !SMAPI_3_0_STRICT
yield return new EventFinder(typeof(SpecialisedEvents).FullName, nameof(SpecialisedEvents.UnvalidatedUpdateTick), InstructionHandleResult.DetectedUnvalidatedUpdateTick);
#endif
diff --git a/src/StardewModdingAPI.Toolkit/Framework/ModScanning/ModScanner.cs b/src/StardewModdingAPI.Toolkit/Framework/ModScanning/ModScanner.cs
index 7624894c..0ab73d56 100644
--- a/src/StardewModdingAPI.Toolkit/Framework/ModScanning/ModScanner.cs
+++ b/src/StardewModdingAPI.Toolkit/Framework/ModScanning/ModScanner.cs
@@ -67,7 +67,7 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning
if (!files.Any())
return new ModFolder(root, searchFolder, null, "it's an empty folder.");
if (files.All(file => this.PotentialXnbModExtensions.Contains(file.Extension)))
- return new ModFolder(root, searchFolder, null, "it's an older XNB mod which replaces game files (not run through SMAPI).");
+ return new ModFolder(root, searchFolder, null, "it's not a SMAPI mod (see https://smapi.io/xnb for info).");
return new ModFolder(root, searchFolder, null, "it contains files, but none of them are manifest.json.");
}