summaryrefslogtreecommitdiff
path: root/src/SMAPI
diff options
context:
space:
mode:
Diffstat (limited to 'src/SMAPI')
-rw-r--r--src/SMAPI/Constants.cs3
-rw-r--r--src/SMAPI/Framework/Content/ContentCache.cs1
-rw-r--r--src/SMAPI/Framework/ContentCoordinator.cs95
-rw-r--r--src/SMAPI/Framework/GameVersion.cs30
-rw-r--r--src/SMAPI/Framework/InternalExtensions.cs70
-rw-r--r--src/SMAPI/Framework/ModHelpers/DataHelper.cs2
-rw-r--r--src/SMAPI/Framework/SCore.cs7
-rw-r--r--src/SMAPI/Framework/SnapshotListDiff.cs6
-rw-r--r--src/SMAPI/Metadata/CoreAssetPropagator.cs53
-rw-r--r--src/SMAPI/Patches/LoadErrorPatch.cs87
-rw-r--r--src/SMAPI/SMAPI.config.json6
-rw-r--r--src/SMAPI/SemanticVersion.cs34
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>