diff options
Diffstat (limited to 'src/SMAPI')
-rw-r--r-- | src/SMAPI/Constants.cs | 3 | ||||
-rw-r--r-- | src/SMAPI/Framework/Content/ContentCache.cs | 1 | ||||
-rw-r--r-- | src/SMAPI/Framework/ContentCoordinator.cs | 95 | ||||
-rw-r--r-- | src/SMAPI/Framework/GameVersion.cs | 30 | ||||
-rw-r--r-- | src/SMAPI/Framework/InternalExtensions.cs | 70 | ||||
-rw-r--r-- | src/SMAPI/Framework/ModHelpers/DataHelper.cs | 2 | ||||
-rw-r--r-- | src/SMAPI/Framework/SCore.cs | 7 | ||||
-rw-r--r-- | src/SMAPI/Framework/SnapshotListDiff.cs | 6 | ||||
-rw-r--r-- | src/SMAPI/Metadata/CoreAssetPropagator.cs | 53 | ||||
-rw-r--r-- | src/SMAPI/Patches/LoadErrorPatch.cs | 87 | ||||
-rw-r--r-- | src/SMAPI/SMAPI.config.json | 6 | ||||
-rw-r--r-- | src/SMAPI/SemanticVersion.cs | 34 |
12 files changed, 300 insertions, 94 deletions
diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs index 67c7b576..8afe4b52 100644 --- a/src/SMAPI/Constants.cs +++ b/src/SMAPI/Constants.cs @@ -64,6 +64,9 @@ namespace StardewModdingAPI /// <summary>The file path for the SMAPI configuration file.</summary> internal static string ApiConfigPath => Path.Combine(Constants.InternalFilesPath, "config.json"); + /// <summary>The file path for the overrides file for <see cref="ApiConfigPath"/>, which is applied over it.</summary> + internal static string ApiUserConfigPath => Path.Combine(Constants.InternalFilesPath, "config.user.json"); + /// <summary>The file path for the SMAPI metadata file.</summary> internal static string ApiMetadataPath => Path.Combine(Constants.InternalFilesPath, "metadata.json"); diff --git a/src/SMAPI/Framework/Content/ContentCache.cs b/src/SMAPI/Framework/Content/ContentCache.cs index f33ff84d..b0933ac6 100644 --- a/src/SMAPI/Framework/Content/ContentCache.cs +++ b/src/SMAPI/Framework/Content/ContentCache.cs @@ -4,7 +4,6 @@ using System.Diagnostics.Contracts; using System.Linq; using Microsoft.Xna.Framework; using StardewModdingAPI.Framework.Reflection; -using StardewModdingAPI.Internal; using StardewModdingAPI.Toolkit.Utilities; using StardewValley; diff --git a/src/SMAPI/Framework/ContentCoordinator.cs b/src/SMAPI/Framework/ContentCoordinator.cs index 82d3805b..b60483f1 100644 --- a/src/SMAPI/Framework/ContentCoordinator.cs +++ b/src/SMAPI/Framework/ContentCoordinator.cs @@ -1,8 +1,10 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.IO; using System.Linq; +using System.Threading; using Microsoft.Xna.Framework.Content; using StardewModdingAPI.Framework.Content; using StardewModdingAPI.Framework.ContentManagers; @@ -48,6 +50,10 @@ namespace StardewModdingAPI.Framework /// <summary>Whether the content coordinator has been disposed.</summary> private bool IsDisposed; + /// <summary>A lock used to prevent asynchronous changes to the content manager list.</summary> + /// <remarks>The game may adds content managers in asynchronous threads (e.g. when populating the load screen).</remarks> + private readonly ReaderWriterLockSlim ContentManagerLock = new ReaderWriterLockSlim(); + /********* ** Accessors @@ -96,9 +102,12 @@ namespace StardewModdingAPI.Framework /// <param name="name">A name for the mod manager. Not guaranteed to be unique.</param> public GameContentManager CreateGameContentManager(string name) { - GameContentManager manager = new GameContentManager(name, this.MainContentManager.ServiceProvider, this.MainContentManager.RootDirectory, this.MainContentManager.CurrentCulture, this, this.Monitor, this.Reflection, this.OnDisposing, this.OnLoadingFirstAsset); - this.ContentManagers.Add(manager); - return manager; + return this.ContentManagerLock.InWriteLock(() => + { + GameContentManager manager = new GameContentManager(name, this.MainContentManager.ServiceProvider, this.MainContentManager.RootDirectory, this.MainContentManager.CurrentCulture, this, this.Monitor, this.Reflection, this.OnDisposing, this.OnLoadingFirstAsset); + this.ContentManagers.Add(manager); + return manager; + }); } /// <summary>Get a new content manager which handles reading files from a SMAPI mod folder with support for unpacked files.</summary> @@ -107,20 +116,23 @@ namespace StardewModdingAPI.Framework /// <param name="gameContentManager">The game content manager used for map tilesheets not provided by the mod.</param> public ModContentManager CreateModContentManager(string name, string rootDirectory, IContentManager gameContentManager) { - ModContentManager manager = new ModContentManager( - name: name, - gameContentManager: gameContentManager, - serviceProvider: this.MainContentManager.ServiceProvider, - rootDirectory: rootDirectory, - currentCulture: this.MainContentManager.CurrentCulture, - coordinator: this, - monitor: this.Monitor, - reflection: this.Reflection, - jsonHelper: this.JsonHelper, - onDisposing: this.OnDisposing - ); - this.ContentManagers.Add(manager); - return manager; + return this.ContentManagerLock.InWriteLock(() => + { + ModContentManager manager = new ModContentManager( + name: name, + gameContentManager: gameContentManager, + serviceProvider: this.MainContentManager.ServiceProvider, + rootDirectory: rootDirectory, + currentCulture: this.MainContentManager.CurrentCulture, + coordinator: this, + monitor: this.Monitor, + reflection: this.Reflection, + jsonHelper: this.JsonHelper, + onDisposing: this.OnDisposing + ); + this.ContentManagers.Add(manager); + return manager; + }); } /// <summary>Get the current content locale.</summary> @@ -132,8 +144,11 @@ namespace StardewModdingAPI.Framework /// <summary>Perform any cleanup needed when the locale changes.</summary> public void OnLocaleChanged() { - foreach (IContentManager contentManager in this.ContentManagers) - contentManager.OnLocaleChanged(); + this.ContentManagerLock.InReadLock(() => + { + foreach (IContentManager contentManager in this.ContentManagers) + contentManager.OnLocaleChanged(); + }); } /// <summary>Get whether this asset is mapped to a mod folder.</summary> @@ -180,7 +195,9 @@ namespace StardewModdingAPI.Framework public T LoadManagedAsset<T>(string contentManagerID, string relativePath) { // get content manager - IContentManager contentManager = this.ContentManagers.FirstOrDefault(p => p.IsNamespaced && p.Name == contentManagerID); + IContentManager contentManager = this.ContentManagerLock.InReadLock(() => + this.ContentManagers.FirstOrDefault(p => p.IsNamespaced && p.Name == contentManagerID) + ); if (contentManager == null) throw new InvalidOperationException($"The '{contentManagerID}' prefix isn't handled by any mod."); @@ -210,15 +227,18 @@ namespace StardewModdingAPI.Framework { // invalidate cache & track removed assets IDictionary<string, ISet<object>> removedAssets = new Dictionary<string, ISet<object>>(StringComparer.InvariantCultureIgnoreCase); - foreach (IContentManager contentManager in this.ContentManagers) + this.ContentManagerLock.InReadLock(() => { - foreach (var entry in contentManager.InvalidateCache(predicate, dispose)) + foreach (IContentManager contentManager in this.ContentManagers) { - if (!removedAssets.TryGetValue(entry.Key, out ISet<object> assets)) - removedAssets[entry.Key] = assets = new HashSet<object>(new ObjectReferenceComparer<object>()); - assets.Add(entry.Value); + foreach (var entry in contentManager.InvalidateCache(predicate, dispose)) + { + if (!removedAssets.TryGetValue(entry.Key, out ISet<object> assets)) + removedAssets[entry.Key] = assets = new HashSet<object>(new ObjectReferenceComparer<object>()); + assets.Add(entry.Value); + } } - } + }); // reload core game assets if (removedAssets.Any()) @@ -232,6 +252,23 @@ namespace StardewModdingAPI.Framework return removedAssets.Keys; } + /// <summary>Get all loaded instances of an asset name.</summary> + /// <param name="assetName">The asset name.</param> + [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = "This method is provided for Content Patcher.")] + public IEnumerable<object> GetLoadedValues(string assetName) + { + return this.ContentManagerLock.InReadLock(() => + { + List<object> values = new List<object>(); + foreach (IContentManager content in this.ContentManagers.Where(p => !p.IsNamespaced && p.IsLoaded(assetName))) + { + object value = content.Load<object>(assetName, this.Language, useCache: true); + values.Add(value); + } + return values; + }); + } + /// <summary>Dispose held resources.</summary> public void Dispose() { @@ -244,6 +281,8 @@ namespace StardewModdingAPI.Framework contentManager.Dispose(); this.ContentManagers.Clear(); this.MainContentManager = null; + + this.ContentManagerLock.Dispose(); } @@ -257,7 +296,9 @@ namespace StardewModdingAPI.Framework if (this.IsDisposed) return; - this.ContentManagers.Remove(contentManager); + this.ContentManagerLock.InWriteLock(() => + this.ContentManagers.Remove(contentManager) + ); } } } diff --git a/src/SMAPI/Framework/GameVersion.cs b/src/SMAPI/Framework/GameVersion.cs index 29cfbc39..07957624 100644 --- a/src/SMAPI/Framework/GameVersion.cs +++ b/src/SMAPI/Framework/GameVersion.cs @@ -3,8 +3,8 @@ using System.Collections.Generic; namespace StardewModdingAPI.Framework { - /// <summary>An implementation of <see cref="ISemanticVersion"/> that correctly handles the non-semantic versions used by older Stardew Valley releases.</summary> - internal class GameVersion : SemanticVersion + /// <summary>An extension of <see cref="ISemanticVersion"/> that correctly handles non-semantic versions used by Stardew Valley.</summary> + internal class GameVersion : Toolkit.SemanticVersion { /********* ** Private methods @@ -18,11 +18,11 @@ namespace StardewModdingAPI.Framework ["1.03"] = "1.0.3", ["1.04"] = "1.0.4", ["1.05"] = "1.0.5", - ["1.051"] = "1.0.6-prerelease1", // not a very good mapping, but good enough for SMAPI's purposes. - ["1.051b"] = "1.0.6-prerelease2", + ["1.051"] = "1.0.5.1", + ["1.051b"] = "1.0.5.2", ["1.06"] = "1.0.6", ["1.07"] = "1.0.7", - ["1.07a"] = "1.0.8-prerelease1", + ["1.07a"] = "1.0.7.1", ["1.08"] = "1.0.8", ["1.1"] = "1.1.0", ["1.2"] = "1.2.0", @@ -36,7 +36,7 @@ namespace StardewModdingAPI.Framework /// <summary>Construct an instance.</summary> /// <param name="version">The game version string.</param> public GameVersion(string version) - : base(GameVersion.GetSemanticVersionString(version)) { } + : base(GameVersion.GetSemanticVersionString(version), allowNonStandard: true) { } /// <summary>Get a string representation of the version.</summary> public override string ToString() @@ -53,33 +53,21 @@ namespace StardewModdingAPI.Framework private static string GetSemanticVersionString(string gameVersion) { // mapped version - if (GameVersion.VersionMap.TryGetValue(gameVersion, out string semanticVersion)) - return semanticVersion; - - // special case: four-part versions - string[] parts = gameVersion.Split('.'); - if (parts.Length == 4) - return $"{parts[0]}.{parts[1]}.{parts[2]}+{parts[3]}"; - - return gameVersion; + return GameVersion.VersionMap.TryGetValue(gameVersion, out string semanticVersion) + ? semanticVersion + : gameVersion; } /// <summary>Convert a semantic version string to the equivalent game version string.</summary> /// <param name="semanticVersion">The semantic version string.</param> private static string GetGameVersionString(string semanticVersion) { - // mapped versions foreach (var mapping in GameVersion.VersionMap) { if (mapping.Value.Equals(semanticVersion, StringComparison.InvariantCultureIgnoreCase)) return mapping.Key; } - // special case: four-part versions - string[] parts = semanticVersion.Split('.', '+'); - if (parts.Length == 4) - return $"{parts[0]}.{parts[1]}.{parts[2]}.{parts[3]}"; - return semanticVersion; } } diff --git a/src/SMAPI/Framework/InternalExtensions.cs b/src/SMAPI/Framework/InternalExtensions.cs index c3155b1c..8b45e196 100644 --- a/src/SMAPI/Framework/InternalExtensions.cs +++ b/src/SMAPI/Framework/InternalExtensions.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Reflection; +using System.Threading; using Microsoft.Xna.Framework.Graphics; using StardewModdingAPI.Framework.Events; using StardewModdingAPI.Framework.Reflection; @@ -84,6 +85,75 @@ namespace StardewModdingAPI.Framework } /**** + ** ReaderWriterLockSlim + ****/ + /// <summary>Run code within a read lock.</summary> + /// <param name="lock">The lock to set.</param> + /// <param name="action">The action to perform.</param> + public static void InReadLock(this ReaderWriterLockSlim @lock, Action action) + { + @lock.EnterReadLock(); + try + { + action(); + } + finally + { + @lock.ExitReadLock(); + } + } + + /// <summary>Run code within a read lock.</summary> + /// <typeparam name="TReturn">The action's return value.</typeparam> + /// <param name="lock">The lock to set.</param> + /// <param name="action">The action to perform.</param> + public static TReturn InReadLock<TReturn>(this ReaderWriterLockSlim @lock, Func<TReturn> action) + { + @lock.EnterReadLock(); + try + { + return action(); + } + finally + { + @lock.ExitReadLock(); + } + } + + /// <summary>Run code within a write lock.</summary> + /// <param name="lock">The lock to set.</param> + /// <param name="action">The action to perform.</param> + public static void InWriteLock(this ReaderWriterLockSlim @lock, Action action) + { + @lock.EnterWriteLock(); + try + { + action(); + } + finally + { + @lock.ExitWriteLock(); + } + } + + /// <summary>Run code within a write lock.</summary> + /// <typeparam name="TReturn">The action's return value.</typeparam> + /// <param name="lock">The lock to set.</param> + /// <param name="action">The action to perform.</param> + public static TReturn InWriteLock<TReturn>(this ReaderWriterLockSlim @lock, Func<TReturn> action) + { + @lock.EnterWriteLock(); + try + { + return action(); + } + finally + { + @lock.ExitWriteLock(); + } + } + + /**** ** Sprite batch ****/ /// <summary>Get whether the sprite batch is between a begin and end pair.</summary> diff --git a/src/SMAPI/Framework/ModHelpers/DataHelper.cs b/src/SMAPI/Framework/ModHelpers/DataHelper.cs index 3d43c539..6cde849c 100644 --- a/src/SMAPI/Framework/ModHelpers/DataHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/DataHelper.cs @@ -177,7 +177,7 @@ namespace StardewModdingAPI.Framework.ModHelpers private string GetGlobalDataPath(string key) { this.AssertSlug(key, nameof(key)); - return Path.Combine(Constants.SavesPath, ".smapi", "mod-data", this.ModID.ToLower(), $"{key}.json".ToLower()); + return Path.Combine(Constants.DataPath, ".smapi", "mod-data", this.ModID.ToLower(), $"{key}.json".ToLower()); } /// <summary>Assert that a key contains only characters that are safe in all contexts.</summary> diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index f996ae97..9139b371 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -158,6 +158,9 @@ namespace StardewModdingAPI.Framework // init basics this.Settings = JsonConvert.DeserializeObject<SConfig>(File.ReadAllText(Constants.ApiConfigPath)); + if (File.Exists(Constants.ApiUserConfigPath)) + JsonConvert.PopulateObject(File.ReadAllText(Constants.ApiUserConfigPath), this.Settings); + this.LogFile = new LogFileManager(logPath); this.Monitor = new Monitor("SMAPI", this.ConsoleManager, this.LogFile, this.Settings.ConsoleColors, this.Settings.VerboseLogging) { @@ -318,7 +321,7 @@ namespace StardewModdingAPI.Framework // show details if game crashed during last session if (File.Exists(Constants.FatalCrashMarker)) { - this.Monitor.Log("The game crashed last time you played. That can be due to bugs in the game, but if it happens repeatedly you can ask for help here: https://community.playstarbound.com/threads/108375/.", LogLevel.Error); + this.Monitor.Log("The game crashed last time you played. If it happens repeatedly, see 'get help' on https://smapi.io.", LogLevel.Error); this.Monitor.Log("If you ask for help, make sure to share your SMAPI log: https://smapi.io/log.", LogLevel.Error); this.Monitor.Log("Press any key to delete the crash data and continue playing.", LogLevel.Info); Console.ReadKey(); @@ -600,6 +603,8 @@ namespace StardewModdingAPI.Framework else this.Monitor.Log(" SMAPI okay.", LogLevel.Trace); + updateFound = response.SuggestedUpdate?.Version; + // show errors if (response.Errors.Any()) { diff --git a/src/SMAPI/Framework/SnapshotListDiff.cs b/src/SMAPI/Framework/SnapshotListDiff.cs index d4d5df50..2d0efa0d 100644 --- a/src/SMAPI/Framework/SnapshotListDiff.cs +++ b/src/SMAPI/Framework/SnapshotListDiff.cs @@ -42,10 +42,12 @@ namespace StardewModdingAPI.Framework this.IsChanged = isChanged; this.RemovedImpl.Clear(); - this.RemovedImpl.AddRange(removed); + if (removed != null) + this.RemovedImpl.AddRange(removed); this.AddedImpl.Clear(); - this.AddedImpl.AddRange(added); + if (added != null) + this.AddedImpl.AddRange(added); } /// <summary>Update the snapshot.</summary> diff --git a/src/SMAPI/Metadata/CoreAssetPropagator.cs b/src/SMAPI/Metadata/CoreAssetPropagator.cs index b86a6790..7a58d52c 100644 --- a/src/SMAPI/Metadata/CoreAssetPropagator.cs +++ b/src/SMAPI/Metadata/CoreAssetPropagator.cs @@ -190,17 +190,9 @@ namespace StardewModdingAPI.Metadata case "characters\\farmer\\farmer_base": // Farmer case "characters\\farmer\\farmer_base_bald": - if (Game1.player == null || !Game1.player.IsMale) - return false; - Game1.player.FarmerRenderer = new FarmerRenderer(key, Game1.player); - return true; - - case "characters\\farmer\\farmer_girl_base": // Farmer + case "characters\\farmer\\farmer_girl_base": case "characters\\farmer\\farmer_girl_base_bald": - if (Game1.player == null || Game1.player.IsMale) - return false; - Game1.player.FarmerRenderer = new FarmerRenderer(key, Game1.player); - return true; + return this.ReloadPlayerSprites(key); case "characters\\farmer\\hairstyles": // Game1.LoadContent FarmerRenderer.hairStylesTexture = content.Load<Texture2D>(key); @@ -835,6 +827,27 @@ namespace StardewModdingAPI.Metadata } } + /// <summary>Reload the sprites for matching players.</summary> + /// <param name="key">The asset key to reload.</param> + private bool ReloadPlayerSprites(string key) + { + Farmer[] players = + ( + from player in Game1.getOnlineFarmers() + where key == this.NormalizeAssetNameIgnoringEmpty(player.getTexture()) + select player + ) + .ToArray(); + + foreach (Farmer player in players) + { + this.Reflection.GetField<Dictionary<string, Dictionary<int, List<int>>>>(typeof(FarmerRenderer), "_recolorOffsets").GetValue().Remove(player.getTexture()); + player.FarmerRenderer.MarkSpriteDirty(); + } + + return players.Any(); + } + /// <summary>Reload tree textures.</summary> /// <param name="content">The content manager through which to reload the asset.</param> /// <param name="key">The asset key to reload.</param> @@ -874,7 +887,11 @@ namespace StardewModdingAPI.Metadata // update dialogue foreach (NPC villager in villagers) + { villager.resetSeasonalDialogue(); // doesn't only affect seasonal dialogue + villager.resetCurrentDialogue(); + } + return true; } @@ -896,18 +913,16 @@ namespace StardewModdingAPI.Metadata this.Reflection.GetField<bool>(villager, "_hasLoadedMasterScheduleData").SetValue(false); this.Reflection.GetField<Dictionary<string, string>>(villager, "_masterScheduleData").SetValue(null); villager.Schedule = villager.getSchedule(Game1.dayOfMonth); - if (villager.Schedule == null) - { - this.Monitor.Log($"A mod set an invalid schedule for {villager.Name ?? key}, so the NPC may not behave correctly.", LogLevel.Warn); - return true; - } // switch to new schedule if needed - int lastScheduleTime = villager.Schedule.Keys.Where(p => p <= Game1.timeOfDay).OrderByDescending(p => p).FirstOrDefault(); - if (lastScheduleTime != 0) + if (villager.Schedule != null) { - villager.scheduleTimeToTry = NPC.NO_TRY; // use time that's passed in to checkSchedule - villager.checkSchedule(lastScheduleTime); + int lastScheduleTime = villager.Schedule.Keys.Where(p => p <= Game1.timeOfDay).OrderByDescending(p => p).FirstOrDefault(); + if (lastScheduleTime != 0) + { + villager.scheduleTimeToTry = NPC.NO_TRY; // use time that's passed in to checkSchedule + villager.checkSchedule(lastScheduleTime); + } } } return true; diff --git a/src/SMAPI/Patches/LoadErrorPatch.cs b/src/SMAPI/Patches/LoadErrorPatch.cs index eedb4164..c16ca7cc 100644 --- a/src/SMAPI/Patches/LoadErrorPatch.cs +++ b/src/SMAPI/Patches/LoadErrorPatch.cs @@ -3,8 +3,10 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; using Harmony; +using StardewModdingAPI.Framework.Exceptions; using StardewModdingAPI.Framework.Patching; using StardewValley; +using StardewValley.Buildings; using StardewValley.Locations; namespace StardewModdingAPI.Patches @@ -64,10 +66,24 @@ namespace StardewModdingAPI.Patches /// <returns>Returns whether to execute the original method.</returns> private static bool Before_SaveGame_LoadDataToLocations(List<GameLocation> gamelocations) { + bool removedAny = + LoadErrorPatch.RemoveInvalidLocations(gamelocations) + | LoadErrorPatch.RemoveBrokenBuildings(gamelocations) + | LoadErrorPatch.RemoveInvalidNpcs(gamelocations); + + if (removedAny) + LoadErrorPatch.OnContentRemoved(); + + return true; + } + + /// <summary>Remove locations which don't exist in-game.</summary> + /// <param name="locations">The current game locations.</param> + private static bool RemoveInvalidLocations(List<GameLocation> locations) + { bool removedAny = false; - // remove invalid locations - foreach (GameLocation location in gamelocations.ToArray()) + foreach (GameLocation location in locations.ToArray()) { if (location is Cellar) continue; // missing cellars will be added by the game code @@ -75,23 +91,48 @@ namespace StardewModdingAPI.Patches if (Game1.getLocationFromName(location.name) == null) { LoadErrorPatch.Monitor.Log($"Removed invalid location '{location.Name}' to avoid a crash when loading save '{Constants.SaveFolderName}'. (Did you remove a custom location mod?)", LogLevel.Warn); - gamelocations.Remove(location); + locations.Remove(location); removedAny = true; } } - // get building interiors - var interiors = - ( - from location in gamelocations.OfType<BuildableGameLocation>() - from building in location.buildings - where building.indoors.Value != null - select building.indoors.Value - ); + return removedAny; + } + + /// <summary>Remove buildings which don't exist in the game data.</summary> + /// <param name="locations">The current game locations.</param> + private static bool RemoveBrokenBuildings(IEnumerable<GameLocation> locations) + { + bool removedAny = false; + + foreach (BuildableGameLocation location in locations.OfType<BuildableGameLocation>()) + { + foreach (Building building in location.buildings.ToArray()) + { + try + { + BluePrint _ = new BluePrint(building.buildingType.Value); + } + catch (SContentLoadException) + { + LoadErrorPatch.Monitor.Log($"Removed invalid building type '{building.buildingType.Value}' in {location.Name} ({building.tileX}, {building.tileY}) to avoid a crash when loading save '{Constants.SaveFolderName}'. (Did you remove a custom building mod?)", LogLevel.Warn); + location.buildings.Remove(building); + removedAny = true; + } + } + } + + return removedAny; + } + + /// <summary>Remove NPCs which don't exist in the game data.</summary> + /// <param name="locations">The current game locations.</param> + private static bool RemoveInvalidNpcs(IEnumerable<GameLocation> locations) + { + bool removedAny = false; - // remove custom NPCs which no longer exist IDictionary<string, string> data = Game1.content.Load<Dictionary<string, string>>("Data\\NPCDispositions"); - foreach (GameLocation location in gamelocations.Concat(interiors)) + foreach (GameLocation location in LoadErrorPatch.GetAllLocations(locations)) { foreach (NPC npc in location.characters.ToArray()) { @@ -103,7 +144,7 @@ namespace StardewModdingAPI.Patches } catch { - LoadErrorPatch.Monitor.Log($"Removed invalid villager '{npc.Name}' to avoid a crash when loading save '{Constants.SaveFolderName}'. (Did you remove a custom NPC mod?)", LogLevel.Warn); + LoadErrorPatch.Monitor.Log($"Removed invalid villager '{npc.Name}' in {location.Name} ({npc.getTileLocation()}) to avoid a crash when loading save '{Constants.SaveFolderName}'. (Did you remove a custom NPC mod?)", LogLevel.Warn); location.characters.Remove(npc); removedAny = true; } @@ -111,10 +152,22 @@ namespace StardewModdingAPI.Patches } } - if (removedAny) - LoadErrorPatch.OnContentRemoved(); + return removedAny; + } - return true; + /// <summary>Get all locations, including building interiors.</summary> + /// <param name="locations">The main game locations.</param> + private static IEnumerable<GameLocation> GetAllLocations(IEnumerable<GameLocation> locations) + { + foreach (GameLocation location in locations) + { + yield return location; + if (location is BuildableGameLocation buildableLocation) + { + foreach (GameLocation interior in buildableLocation.buildings.Select(p => p.indoors.Value).Where(p => p != null)) + yield return interior; + } + } } } } diff --git a/src/SMAPI/SMAPI.config.json b/src/SMAPI/SMAPI.config.json index 824bb783..57b4f885 100644 --- a/src/SMAPI/SMAPI.config.json +++ b/src/SMAPI/SMAPI.config.json @@ -6,6 +6,12 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha The default values are mirrored in StardewModdingAPI.Framework.Models.SConfig to log custom changes. +This file is overwritten each time you update or reinstall SMAPI. To avoid losing custom settings, +create a 'config.user.json' file in the same folder with *only* the settings you want to change. +That file won't be overwritten, and any settings in it will override the default options. Don't +copy all the settings, or you may cause bugs due to overridden changes in future SMAPI versions. + + */ { diff --git a/src/SMAPI/SemanticVersion.cs b/src/SMAPI/SemanticVersion.cs index 2a33ecef..4a175efe 100644 --- a/src/SMAPI/SemanticVersion.cs +++ b/src/SMAPI/SemanticVersion.cs @@ -39,18 +39,36 @@ namespace StardewModdingAPI /// <param name="majorVersion">The major version incremented for major API changes.</param> /// <param name="minorVersion">The minor version incremented for backwards-compatible changes.</param> /// <param name="patchVersion">The patch version for backwards-compatible bug fixes.</param> - /// <param name="prerelease">An optional prerelease tag.</param> - /// <param name="build">Optional build metadata. This is ignored when determining version precedence.</param> + /// <param name="prereleaseTag">An optional prerelease tag.</param> + /// <param name="buildMetadata">Optional build metadata. This is ignored when determining version precedence.</param> + public SemanticVersion(int majorVersion, int minorVersion, int patchVersion, string prereleaseTag = null, string buildMetadata = null) + : this(majorVersion, minorVersion, patchVersion, 0, prereleaseTag, buildMetadata) { } + + /// <summary>Construct an instance.</summary> + /// <param name="majorVersion">The major version incremented for major API changes.</param> + /// <param name="minorVersion">The minor version incremented for backwards-compatible changes.</param> + /// <param name="patchVersion">The patch version for backwards-compatible bug fixes.</param> + /// <param name="prereleaseTag">An optional prerelease tag.</param> + /// <param name="platformRelease">The platform-specific version (if applicable).</param> + /// <param name="buildMetadata">Optional build metadata. This is ignored when determining version precedence.</param> [JsonConstructor] - public SemanticVersion(int majorVersion, int minorVersion, int patchVersion, string prerelease = null, string build = null) - : this(new Toolkit.SemanticVersion(majorVersion, minorVersion, patchVersion, prerelease, build)) { } + internal SemanticVersion(int majorVersion, int minorVersion, int patchVersion, int platformRelease, string prereleaseTag = null, string buildMetadata = null) + : this(new Toolkit.SemanticVersion(majorVersion, minorVersion, patchVersion, platformRelease, prereleaseTag, buildMetadata)) { } /// <summary>Construct an instance.</summary> /// <param name="version">The semantic version string.</param> /// <exception cref="ArgumentNullException">The <paramref name="version"/> is null.</exception> /// <exception cref="FormatException">The <paramref name="version"/> is not a valid semantic version.</exception> public SemanticVersion(string version) - : this(new Toolkit.SemanticVersion(version)) { } + : this(version, allowNonStandard: false) { } + + /// <summary>Construct an instance.</summary> + /// <param name="version">The semantic version string.</param> + /// <param name="allowNonStandard">Whether to recognize non-standard semver extensions.</param> + /// <exception cref="ArgumentNullException">The <paramref name="version"/> is null.</exception> + /// <exception cref="FormatException">The <paramref name="version"/> is not a valid semantic version.</exception> + internal SemanticVersion(string version, bool allowNonStandard) + : this(new Toolkit.SemanticVersion(version, allowNonStandard)) { } /// <summary>Construct an instance.</summary> /// <param name="version">The assembly version.</param> @@ -141,6 +159,12 @@ namespace StardewModdingAPI return this.Version.ToString(); } + /// <summary>Whether the version uses non-standard extensions, like four-part game versions on some platforms.</summary> + public bool IsNonStandard() + { + return this.Version.IsNonStandard(); + } + /// <summary>Parse a version string without throwing an exception if it fails.</summary> /// <param name="version">The version string.</param> /// <param name="parsed">The parsed representation.</param> |