fix errors due to async threads creating content managers

This commit is contained in:
Jesse Plamondon-Willard 2020-01-11 13:20:37 -05:00
parent ceff27c9a8
commit 219696275d
No known key found for this signature in database
GPG Key ID: CF8B1456B3E29F49
3 changed files with 139 additions and 43 deletions

View File

@ -6,6 +6,7 @@
* For players:
* SMAPI now prevents mods from crashing the game with invalid schedule data.
* Updated minimum game version (1.4 → 1.4.1).
* Fixed 'collection was modified' error when returning to title in rare cases.
## 3.1
Released 05 January 2019 for Stardew Valley 1.4 or later.

View File

@ -3,6 +3,7 @@ using System.Collections.Generic;
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 +49,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
@ -95,10 +100,13 @@ namespace StardewModdingAPI.Framework
/// <summary>Get a new content manager which handles reading files from the game content folder with support for interception.</summary>
/// <param name="name">A name for the mod manager. Not guaranteed to be unique.</param>
public GameContentManager CreateGameContentManager(string name)
{
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>
@ -106,6 +114,8 @@ namespace StardewModdingAPI.Framework
/// <param name="rootDirectory">The root directory to search for content (or <c>null</c> for the default).</param>
/// <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)
{
return this.ContentManagerLock.InWriteLock(() =>
{
ModContentManager manager = new ModContentManager(
name: name,
@ -121,6 +131,7 @@ namespace StardewModdingAPI.Framework
);
this.ContentManagers.Add(manager);
return manager;
});
}
/// <summary>Get the current content locale.</summary>
@ -131,9 +142,12 @@ namespace StardewModdingAPI.Framework
/// <summary>Perform any cleanup needed when the locale changes.</summary>
public void OnLocaleChanged()
{
this.ContentManagerLock.InReadLock(() =>
{
foreach (IContentManager contentManager in this.ContentManagers)
contentManager.OnLocaleChanged();
});
}
/// <summary>Get whether this asset is mapped to a mod folder.</summary>
@ -178,6 +192,8 @@ namespace StardewModdingAPI.Framework
/// <param name="contentManagerID">The unique name for the content manager which should load this asset.</param>
/// <param name="relativePath">The internal SMAPI asset key.</param>
public T LoadManagedAsset<T>(string contentManagerID, string relativePath)
{
return this.ContentManagerLock.InReadLock(() =>
{
// get content manager
IContentManager contentManager = this.ContentManagers.FirstOrDefault(p => p.IsNamespaced && p.Name == contentManagerID);
@ -186,6 +202,7 @@ namespace StardewModdingAPI.Framework
// get fresh asset
return contentManager.Load<T>(relativePath, this.DefaultLanguage, useCache: false);
});
}
/// <summary>Purge matched assets from the cache.</summary>
@ -207,6 +224,8 @@ namespace StardewModdingAPI.Framework
/// <param name="dispose">Whether to dispose invalidated assets. This should only be <c>true</c> when they're being invalidated as part of a dispose, to avoid crashing the game.</param>
/// <returns>Returns the invalidated asset names.</returns>
public IEnumerable<string> InvalidateCache(Func<string, Type, bool> predicate, bool dispose = false)
{
return this.ContentManagerLock.InReadLock(() =>
{
// invalidate cache & track removed assets
IDictionary<string, ISet<object>> removedAssets = new Dictionary<string, ISet<object>>(StringComparer.InvariantCultureIgnoreCase);
@ -230,6 +249,7 @@ namespace StardewModdingAPI.Framework
this.Monitor.Log("Invalidated 0 cache entries.", LogLevel.Trace);
return removedAssets.Keys;
});
}
/// <summary>Dispose held resources.</summary>
@ -244,6 +264,8 @@ namespace StardewModdingAPI.Framework
contentManager.Dispose();
this.ContentManagers.Clear();
this.MainContentManager = null;
this.ContentManagerLock.Dispose();
}
@ -257,7 +279,10 @@ namespace StardewModdingAPI.Framework
if (this.IsDisposed)
return;
this.ContentManagerLock.InWriteLock(() =>
{
this.ContentManagers.Remove(contentManager);
});
}
}
}

View File

@ -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;
@ -83,6 +84,75 @@ namespace StardewModdingAPI.Framework
return exception;
}
/****
** 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
****/