summaryrefslogtreecommitdiff
path: root/src/SMAPI/Framework
diff options
context:
space:
mode:
Diffstat (limited to 'src/SMAPI/Framework')
-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
7 files changed, 158 insertions, 53 deletions
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>