summaryrefslogtreecommitdiff
path: root/src/SMAPI/Framework
diff options
context:
space:
mode:
authorJesse Plamondon-Willard <Pathoschild@users.noreply.github.com>2018-12-07 13:40:44 -0500
committerJesse Plamondon-Willard <Pathoschild@users.noreply.github.com>2018-12-07 13:40:44 -0500
commita78b1935928919694dfe8de823a1accd6d222732 (patch)
tree3f17b6087cf2749e52c1e237de17e2e9addb6c06 /src/SMAPI/Framework
parent4cd9eda1591c3908bf80b60c2902491a7595ee27 (diff)
parent8901218418693d610a17b22fe789ba6279f63446 (diff)
downloadSMAPI-a78b1935928919694dfe8de823a1accd6d222732.tar.gz
SMAPI-a78b1935928919694dfe8de823a1accd6d222732.tar.bz2
SMAPI-a78b1935928919694dfe8de823a1accd6d222732.zip
Merge branch 'develop' into stable
Diffstat (limited to 'src/SMAPI/Framework')
-rw-r--r--src/SMAPI/Framework/ContentCoordinator.cs16
-rw-r--r--src/SMAPI/Framework/ContentManagers/BaseContentManager.cs20
-rw-r--r--src/SMAPI/Framework/ContentManagers/GameContentManager.cs27
-rw-r--r--src/SMAPI/Framework/ContentManagers/IContentManager.cs4
-rw-r--r--src/SMAPI/Framework/DeprecationManager.cs6
-rw-r--r--src/SMAPI/Framework/Events/EventManager.cs8
-rw-r--r--src/SMAPI/Framework/ModHelpers/ModHelper.cs36
-rw-r--r--src/SMAPI/Framework/ModLoading/AssemblyLoader.cs9
-rw-r--r--src/SMAPI/Framework/ModLoading/InstructionHandleResult.cs2
-rw-r--r--src/SMAPI/Framework/ModLoading/ModWarning.cs2
-rw-r--r--src/SMAPI/Framework/SCore.cs86
-rw-r--r--src/SMAPI/Framework/SGame.cs127
-rw-r--r--src/SMAPI/Framework/SMultiplayer.cs2
-rw-r--r--src/SMAPI/Framework/StateTracking/FieldWatchers/ComparableListWatcher.cs83
-rw-r--r--src/SMAPI/Framework/StateTracking/FieldWatchers/ComparableWatcher.cs13
-rw-r--r--src/SMAPI/Framework/StateTracking/FieldWatchers/NetCollectionWatcher.cs3
-rw-r--r--src/SMAPI/Framework/StateTracking/FieldWatchers/NetDictionaryWatcher.cs2
-rw-r--r--src/SMAPI/Framework/StateTracking/FieldWatchers/NetValueWatcher.cs14
-rw-r--r--src/SMAPI/Framework/StateTracking/FieldWatchers/ObservableCollectionWatcher.cs3
-rw-r--r--src/SMAPI/Framework/StateTracking/FieldWatchers/WatcherFactory.cs8
-rw-r--r--src/SMAPI/Framework/StateTracking/WorldLocationsTracker.cs50
-rw-r--r--src/SMAPI/Framework/WatcherCore.cs3
22 files changed, 423 insertions, 101 deletions
diff --git a/src/SMAPI/Framework/ContentCoordinator.cs b/src/SMAPI/Framework/ContentCoordinator.cs
index 9eb7b5f9..08a32a9b 100644
--- a/src/SMAPI/Framework/ContentCoordinator.cs
+++ b/src/SMAPI/Framework/ContentCoordinator.cs
@@ -238,28 +238,30 @@ namespace StardewModdingAPI.Framework
public IEnumerable<string> InvalidateCache(Func<string, Type, bool> predicate, bool dispose = false)
{
// invalidate cache
- HashSet<string> removedAssetNames = new HashSet<string>();
+ IDictionary<string, Type> removedAssetNames = new Dictionary<string, Type>(StringComparer.InvariantCultureIgnoreCase);
foreach (IContentManager contentManager in this.ContentManagers)
{
- foreach (string name in contentManager.InvalidateCache(predicate, dispose))
- removedAssetNames.Add(name);
+ foreach (Tuple<string, Type> asset in contentManager.InvalidateCache(predicate, dispose))
+ removedAssetNames[asset.Item1] = asset.Item2;
}
// reload core game assets
int reloaded = 0;
- foreach (string key in removedAssetNames)
+ foreach (var pair in removedAssetNames)
{
- if (this.CoreAssets.Propagate(this.MainContentManager, key)) // use an intercepted content manager
+ string key = pair.Key;
+ Type type = pair.Value;
+ if (this.CoreAssets.Propagate(this.MainContentManager, key, type)) // use an intercepted content manager
reloaded++;
}
// report result
if (removedAssetNames.Any())
- this.Monitor.Log($"Invalidated {removedAssetNames.Count} asset names: {string.Join(", ", removedAssetNames.OrderBy(p => p, StringComparer.InvariantCultureIgnoreCase))}. Reloaded {reloaded} core assets.", LogLevel.Trace);
+ this.Monitor.Log($"Invalidated {removedAssetNames.Count} asset names: {string.Join(", ", removedAssetNames.Keys.OrderBy(p => p, StringComparer.InvariantCultureIgnoreCase))}. Reloaded {reloaded} core assets.", LogLevel.Trace);
else
this.Monitor.Log("Invalidated 0 cache entries.", LogLevel.Trace);
- return removedAssetNames;
+ return removedAssetNames.Keys;
}
/// <summary>Dispose held resources.</summary>
diff --git a/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs b/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs
index 18aae05b..724a6e1c 100644
--- a/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs
+++ b/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs
@@ -32,12 +32,12 @@ namespace StardewModdingAPI.Framework.ContentManagers
/// <summary>Whether the content coordinator has been disposed.</summary>
private bool IsDisposed;
- /// <summary>The language enum values indexed by locale code.</summary>
- private readonly IDictionary<string, LanguageCode> LanguageCodes;
-
/// <summary>A callback to invoke when the content manager is being disposed.</summary>
private readonly Action<BaseContentManager> OnDisposing;
+ /// <summary>The language enum values indexed by locale code.</summary>
+ protected IDictionary<string, LanguageCode> LanguageCodes { get; }
+
/*********
** Accessors
@@ -200,23 +200,25 @@ namespace StardewModdingAPI.Framework.ContentManagers
/// <summary>Purge matched assets from the cache.</summary>
/// <param name="predicate">Matches the asset keys to invalidate.</param>
/// <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 number of invalidated assets.</returns>
- public IEnumerable<string> InvalidateCache(Func<string, Type, bool> predicate, bool dispose = false)
+ /// <returns>Returns the invalidated asset names and types.</returns>
+ public IEnumerable<Tuple<string, Type>> InvalidateCache(Func<string, Type, bool> predicate, bool dispose = false)
{
- HashSet<string> removeAssetNames = new HashSet<string>(StringComparer.InvariantCultureIgnoreCase);
+ Dictionary<string, Type> removeAssetNames = new Dictionary<string, Type>(StringComparer.InvariantCultureIgnoreCase);
this.Cache.Remove((key, type) =>
{
this.ParseCacheKey(key, out string assetName, out _);
- if (removeAssetNames.Contains(assetName) || predicate(assetName, type))
+ if (removeAssetNames.ContainsKey(assetName))
+ return true;
+ if (predicate(assetName, type))
{
- removeAssetNames.Add(assetName);
+ removeAssetNames[assetName] = type;
return true;
}
return false;
});
- return removeAssetNames;
+ return removeAssetNames.Select(p => Tuple.Create(p.Key, p.Value));
}
/// <summary>Dispose held resources.</summary>
diff --git a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs
index a53840bc..4f3b6fbc 100644
--- a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs
+++ b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs
@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using StardewModdingAPI.Framework.Content;
+using StardewModdingAPI.Framework.Exceptions;
using StardewModdingAPI.Framework.Reflection;
using StardewModdingAPI.Framework.Utilities;
using StardewValley;
@@ -52,7 +53,10 @@ namespace StardewModdingAPI.Framework.ContentManagers
/// <param name="language">The language code for which to load content.</param>
public override T Load<T>(string assetName, LanguageCode language)
{
+ // normalise asset name
assetName = this.AssertAndNormaliseAssetName(assetName);
+ if (this.TryParseExplicitLanguageAssetKey(assetName, out string newAssetName, out LanguageCode newLanguage))
+ return this.Load<T>(newAssetName, newLanguage);
// get from cache
if (this.IsLoaded(assetName))
@@ -124,6 +128,29 @@ namespace StardewModdingAPI.Framework.ContentManagers
return false;
}
+ /// <summary>Parse an asset key that contains an explicit language into its asset name and language, if applicable.</summary>
+ /// <param name="rawAsset">The asset key to parse.</param>
+ /// <param name="assetName">The asset name without the language code.</param>
+ /// <param name="language">The language code removed from the asset name.</param>
+ private bool TryParseExplicitLanguageAssetKey(string rawAsset, out string assetName, out LanguageCode language)
+ {
+ if (string.IsNullOrWhiteSpace(rawAsset))
+ throw new SContentLoadException("The asset key is empty.");
+
+ // extract language code
+ int splitIndex = rawAsset.LastIndexOf('.');
+ if (splitIndex != -1 && this.LanguageCodes.TryGetValue(rawAsset.Substring(splitIndex + 1), out language))
+ {
+ assetName = rawAsset.Substring(0, splitIndex);
+ return true;
+ }
+
+ // no explicit language code found
+ assetName = rawAsset;
+ language = this.Language;
+ return false;
+ }
+
/// <summary>Load the initial asset from the registered <see cref="Loaders"/>.</summary>
/// <param name="info">The basic asset metadata.</param>
/// <returns>Returns the loaded asset metadata, or <c>null</c> if no loader matched.</returns>
diff --git a/src/SMAPI/Framework/ContentManagers/IContentManager.cs b/src/SMAPI/Framework/ContentManagers/IContentManager.cs
index 1eb8b0ac..17618edd 100644
--- a/src/SMAPI/Framework/ContentManagers/IContentManager.cs
+++ b/src/SMAPI/Framework/ContentManagers/IContentManager.cs
@@ -80,7 +80,7 @@ namespace StardewModdingAPI.Framework.ContentManagers
/// <summary>Purge matched assets from the cache.</summary>
/// <param name="predicate">Matches the asset keys to invalidate.</param>
/// <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 number of invalidated assets.</returns>
- IEnumerable<string> InvalidateCache(Func<string, Type, bool> predicate, bool dispose = false);
+ /// <returns>Returns the invalidated asset names and types.</returns>
+ IEnumerable<Tuple<string, Type>> InvalidateCache(Func<string, Type, bool> predicate, bool dispose = false);
}
}
diff --git a/src/SMAPI/Framework/DeprecationManager.cs b/src/SMAPI/Framework/DeprecationManager.cs
index 0fde67ee..be564c22 100644
--- a/src/SMAPI/Framework/DeprecationManager.cs
+++ b/src/SMAPI/Framework/DeprecationManager.cs
@@ -35,6 +35,12 @@ namespace StardewModdingAPI.Framework
this.ModRegistry = modRegistry;
}
+ /// <summary>Log a deprecation warning for the old-style events.</summary>
+ public void WarnForOldEvents()
+ {
+ this.Warn("legacy events", "2.9", DeprecationLevel.Notice);
+ }
+
/// <summary>Log a deprecation warning.</summary>
/// <param name="nounPhrase">A noun phrase describing what is deprecated.</param>
/// <param name="version">The SMAPI version which deprecated it.</param>
diff --git a/src/SMAPI/Framework/Events/EventManager.cs b/src/SMAPI/Framework/Events/EventManager.cs
index b9d1c453..0ad85adf 100644
--- a/src/SMAPI/Framework/Events/EventManager.cs
+++ b/src/SMAPI/Framework/Events/EventManager.cs
@@ -1,5 +1,7 @@
using System.Diagnostics.CodeAnalysis;
+#if !SMAPI_3_0_STRICT
using Microsoft.Xna.Framework.Input;
+#endif
using StardewModdingAPI.Events;
namespace StardewModdingAPI.Framework.Events
@@ -156,6 +158,7 @@ namespace StardewModdingAPI.Framework.Events
public readonly ManagedEvent<UnvalidatedUpdateTickedEventArgs> UnvalidatedUpdateTicked;
+#if !SMAPI_3_0_STRICT
/*********
** Events (old)
*********/
@@ -342,6 +345,7 @@ namespace StardewModdingAPI.Framework.Events
/// <summary>Raised after the in-game clock changes.</summary>
public readonly ManagedEvent<EventArgsIntChanged> Legacy_TimeOfDayChanged;
+#endif
/*********
@@ -354,7 +358,9 @@ namespace StardewModdingAPI.Framework.Events
{
// create shortcut initialisers
ManagedEvent<TEventArgs> ManageEventOf<TEventArgs>(string typeName, string eventName) => new ManagedEvent<TEventArgs>($"{typeName}.{eventName}", monitor, modRegistry);
+#if !SMAPI_3_0_STRICT
ManagedEvent ManageEvent(string typeName, string eventName) => new ManagedEvent($"{typeName}.{eventName}", monitor, modRegistry);
+#endif
// init events (new)
this.MenuChanged = ManageEventOf<MenuChangedEventArgs>(nameof(IModEvents.Display), nameof(IDisplayEvents.MenuChanged));
@@ -405,6 +411,7 @@ namespace StardewModdingAPI.Framework.Events
this.UnvalidatedUpdateTicking = ManageEventOf<UnvalidatedUpdateTickingEventArgs>(nameof(IModEvents.Specialised), nameof(ISpecialisedEvents.UnvalidatedUpdateTicking));
this.UnvalidatedUpdateTicked = ManageEventOf<UnvalidatedUpdateTickedEventArgs>(nameof(IModEvents.Specialised), nameof(ISpecialisedEvents.UnvalidatedUpdateTicked));
+#if !SMAPI_3_0_STRICT
// init events (old)
this.Legacy_LocaleChanged = ManageEventOf<EventArgsValueChanged<string>>(nameof(ContentEvents), nameof(ContentEvents.AfterLocaleChanged));
@@ -466,6 +473,7 @@ namespace StardewModdingAPI.Framework.Events
this.Legacy_AfterDayStarted = ManageEvent(nameof(TimeEvents), nameof(TimeEvents.AfterDayStarted));
this.Legacy_TimeOfDayChanged = ManageEventOf<EventArgsIntChanged>(nameof(TimeEvents), nameof(TimeEvents.TimeOfDayChanged));
+#endif
}
}
}
diff --git a/src/SMAPI/Framework/ModHelpers/ModHelper.cs b/src/SMAPI/Framework/ModHelpers/ModHelper.cs
index 5e190e55..070d9c65 100644
--- a/src/SMAPI/Framework/ModHelpers/ModHelper.cs
+++ b/src/SMAPI/Framework/ModHelpers/ModHelper.cs
@@ -21,8 +21,10 @@ namespace StardewModdingAPI.Framework.ModHelpers
/// <summary>Create a transitional content pack.</summary>
private readonly Func<string, IManifest, IContentPack> CreateContentPack;
+#if !SMAPI_3_0_STRICT
/// <summary>Manages deprecation warnings.</summary>
private readonly DeprecationManager DeprecationManager;
+#endif
/*********
@@ -31,8 +33,10 @@ namespace StardewModdingAPI.Framework.ModHelpers
/// <summary>The full path to the mod's folder.</summary>
public string DirectoryPath { get; }
+#if !SMAPI_3_0_STRICT
/// <summary>Encapsulates SMAPI's JSON file parsing.</summary>
private readonly JsonHelper JsonHelper;
+#endif
/// <summary>Manages access to events raised by SMAPI, which let your mod react when something happens in the game.</summary>
public IModEvents Events { get; }
@@ -94,7 +98,6 @@ namespace StardewModdingAPI.Framework.ModHelpers
// initialise
this.DirectoryPath = modDirectory;
- this.JsonHelper = jsonHelper ?? throw new ArgumentNullException(nameof(jsonHelper));
this.Content = contentHelper ?? throw new ArgumentNullException(nameof(contentHelper));
this.Data = dataHelper ?? throw new ArgumentNullException(nameof(dataHelper));
this.Input = new InputHelper(modID, inputState);
@@ -105,8 +108,11 @@ namespace StardewModdingAPI.Framework.ModHelpers
this.Translation = translationHelper ?? throw new ArgumentNullException(nameof(translationHelper));
this.ContentPacks = new Lazy<IContentPack[]>(contentPacks);
this.CreateContentPack = createContentPack;
- this.DeprecationManager = deprecationManager;
this.Events = events;
+#if !SMAPI_3_0_STRICT
+ this.JsonHelper = jsonHelper ?? throw new ArgumentNullException(nameof(jsonHelper));
+ this.DeprecationManager = deprecationManager;
+#endif
}
/****
@@ -131,6 +137,7 @@ namespace StardewModdingAPI.Framework.ModHelpers
this.Data.WriteJsonFile("config.json", config);
}
+#if !SMAPI_3_0_STRICT
/****
** Generic JSON files
****/
@@ -159,23 +166,20 @@ namespace StardewModdingAPI.Framework.ModHelpers
path = Path.Combine(this.DirectoryPath, PathUtilities.NormalisePathSeparators(path));
this.JsonHelper.WriteJsonFile(path, model);
}
+#endif
/****
** Content packs
****/
- /// <summary>Manually create a transitional content pack to support pre-SMAPI content packs. This provides a way to access legacy content packs using the SMAPI content pack APIs, but the content pack will not be visible in the log or validated by SMAPI.</summary>
+ /// <summary>Create a temporary content pack to read files from a directory. Temporary content packs will not appear in the SMAPI log and update checks will not be performed.</summary>
/// <param name="directoryPath">The absolute directory path containing the content pack files.</param>
/// <param name="id">The content pack's unique ID.</param>
/// <param name="name">The content pack name.</param>
/// <param name="description">The content pack description.</param>
/// <param name="author">The content pack author's name.</param>
/// <param name="version">The content pack version.</param>
- [Obsolete("This method supports mods which previously had their own content packs, and shouldn't be used by new mods. It will be removed in SMAPI 3.0.")]
- public IContentPack CreateTransitionalContentPack(string directoryPath, string id, string name, string description, string author, ISemanticVersion version)
+ public IContentPack CreateTemporaryContentPack(string directoryPath, string id, string name, string description, string author, ISemanticVersion version)
{
- // raise deprecation notice
- this.DeprecationManager.Warn($"{nameof(IModHelper)}.{nameof(IModHelper.CreateTransitionalContentPack)}", "2.5", DeprecationLevel.Notice);
-
// validate
if (string.IsNullOrWhiteSpace(directoryPath))
throw new ArgumentNullException(nameof(directoryPath));
@@ -200,6 +204,22 @@ namespace StardewModdingAPI.Framework.ModHelpers
return this.CreateContentPack(directoryPath, manifest);
}
+#if !SMAPI_3_0_STRICT
+ /// <summary>Manually create a transitional content pack to support pre-SMAPI content packs. This provides a way to access legacy content packs using the SMAPI content pack APIs, but the content pack will not be visible in the log or validated by SMAPI.</summary>
+ /// <param name="directoryPath">The absolute directory path containing the content pack files.</param>
+ /// <param name="id">The content pack's unique ID.</param>
+ /// <param name="name">The content pack name.</param>
+ /// <param name="description">The content pack description.</param>
+ /// <param name="author">The content pack author's name.</param>
+ /// <param name="version">The content pack version.</param>
+ [Obsolete("Use " + nameof(IModHelper) + "." + nameof(IModHelper.CreateTemporaryContentPack) + " instead")]
+ public IContentPack CreateTransitionalContentPack(string directoryPath, string id, string name, string description, string author, ISemanticVersion version)
+ {
+ this.DeprecationManager.Warn($"{nameof(IModHelper)}.{nameof(IModHelper.CreateTransitionalContentPack)}", "2.5", DeprecationLevel.Notice);
+ return this.CreateTemporaryContentPack(directoryPath, id, name, description, author, version);
+ }
+#endif
+
/// <summary>Get all content packs loaded for this mod.</summary>
public IEnumerable<IContentPack> GetContentPacks()
{
diff --git a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs
index fdbfdd8d..7292cf3f 100644
--- a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs
+++ b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs
@@ -20,6 +20,9 @@ namespace StardewModdingAPI.Framework.ModLoading
/// <summary>Encapsulates monitoring and logging.</summary>
private readonly IMonitor Monitor;
+ /// <summary>Whether to detect paranoid mode issues.</summary>
+ private readonly bool ParanoidMode;
+
/// <summary>Metadata for mapping assemblies to the current platform.</summary>
private readonly PlatformAssemblyMap AssemblyMap;
@@ -39,9 +42,11 @@ namespace StardewModdingAPI.Framework.ModLoading
/// <summary>Construct an instance.</summary>
/// <param name="targetPlatform">The current game platform.</param>
/// <param name="monitor">Encapsulates monitoring and logging.</param>
- public AssemblyLoader(Platform targetPlatform, IMonitor monitor)
+ /// <param name="paranoidMode">Whether to detect paranoid mode issues.</param>
+ public AssemblyLoader(Platform targetPlatform, IMonitor monitor, bool paranoidMode)
{
this.Monitor = monitor;
+ this.ParanoidMode = paranoidMode;
this.AssemblyMap = this.TrackForDisposal(Constants.GetAssemblyMap(targetPlatform));
this.AssemblyDefinitionResolver = this.TrackForDisposal(new AssemblyDefinitionResolver());
this.AssemblyDefinitionResolver.AddSearchDirectory(Constants.ExecutionPath);
@@ -275,7 +280,7 @@ namespace StardewModdingAPI.Framework.ModLoading
// find (and optionally rewrite) incompatible instructions
bool anyRewritten = false;
- IInstructionHandler[] handlers = new InstructionMetadata().GetHandlers().ToArray();
+ IInstructionHandler[] handlers = new InstructionMetadata().GetHandlers(this.ParanoidMode).ToArray();
foreach (MethodDefinition method in this.GetMethods(module))
{
// check method definition
diff --git a/src/SMAPI/Framework/ModLoading/InstructionHandleResult.cs b/src/SMAPI/Framework/ModLoading/InstructionHandleResult.cs
index f3555c2d..6592760e 100644
--- a/src/SMAPI/Framework/ModLoading/InstructionHandleResult.cs
+++ b/src/SMAPI/Framework/ModLoading/InstructionHandleResult.cs
@@ -23,7 +23,7 @@ namespace StardewModdingAPI.Framework.ModLoading
/// <summary>The instruction is compatible, but uses the <c>dynamic</c> keyword which won't work on Linux/Mac.</summary>
DetectedDynamic,
- /// <summary>The instruction is compatible, but references <see cref="SpecialisedEvents.UnvalidatedUpdateTick"/> which may impact stability.</summary>
+ /// <summary>The instruction is compatible, but references <see cref="ISpecialisedEvents.UnvalidatedUpdateTicking"/> or <see cref="ISpecialisedEvents.UnvalidatedUpdateTicked"/> which may impact stability.</summary>
DetectedUnvalidatedUpdateTick,
/// <summary>The instruction accesses the filesystem directly.</summary>
diff --git a/src/SMAPI/Framework/ModLoading/ModWarning.cs b/src/SMAPI/Framework/ModLoading/ModWarning.cs
index c62199b2..e643cb05 100644
--- a/src/SMAPI/Framework/ModLoading/ModWarning.cs
+++ b/src/SMAPI/Framework/ModLoading/ModWarning.cs
@@ -22,7 +22,7 @@ namespace StardewModdingAPI.Framework.ModLoading
/// <summary>The mod uses the <c>dynamic</c> keyword which won't work on Linux/Mac.</summary>
UsesDynamic = 8,
- /// <summary>The mod references <see cref="SpecialisedEvents.UnvalidatedUpdateTick"/> which may impact stability.</summary>
+ /// <summary>The mod references <see cref="ISpecialisedEvents.UnvalidatedUpdateTicking"/> or <see cref="ISpecialisedEvents.UnvalidatedUpdateTicked"/> which may impact stability.</summary>
UsesUnvalidatedUpdateTick = 16,
/// <summary>The mod has no update keys set.</summary>
diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs
index 4b95917b..800b9c09 100644
--- a/src/SMAPI/Framework/SCore.cs
+++ b/src/SMAPI/Framework/SCore.cs
@@ -99,11 +99,25 @@ namespace StardewModdingAPI.Framework
new Regex(@"^static SerializableDictionary<.+>\(\) called\.$", RegexOptions.Compiled | RegexOptions.CultureInvariant),
};
+ /// <summary>Regex patterns which match console messages to show a more friendly error for.</summary>
+ private readonly Tuple<Regex, string, LogLevel>[] ReplaceConsolePatterns =
+ {
+ Tuple.Create(
+ new Regex(@"^System\.InvalidOperationException: Steamworks is not initialized\.", RegexOptions.Compiled | RegexOptions.CultureInvariant),
+#if SMAPI_FOR_WINDOWS
+ "Oops! Steam achievements won't work because Steam isn't loaded. You can launch the game through Steam to fix that (see 'Part 2: Configure Steam' in the install guide for more info: https://smapi.io/install).",
+#else
+ "Oops! Steam achievements won't work because Steam isn't loaded. You can launch the game through Steam to fix that.",
+#endif
+ LogLevel.Error
+ )
+ };
+
/// <summary>The mod toolkit used for generic mod interactions.</summary>
private readonly ModToolkit Toolkit = new ModToolkit();
/// <summary>The path to search for mods.</summary>
- private readonly string ModsPath;
+ private string ModsPath => Constants.ModsPath;
/*********
@@ -117,7 +131,7 @@ namespace StardewModdingAPI.Framework
// init paths
this.VerifyPath(modsPath);
this.VerifyPath(Constants.LogDir);
- this.ModsPath = modsPath;
+ Constants.ModsPath = modsPath;
// init log file
this.PurgeNormalLogs();
@@ -180,20 +194,22 @@ namespace StardewModdingAPI.Framework
// initialise SMAPI
try
{
+#if !SMAPI_3_0_STRICT
// hook up events
- ContentEvents.Init(this.EventManager);
- ControlEvents.Init(this.EventManager);
- GameEvents.Init(this.EventManager);
- GraphicsEvents.Init(this.EventManager);
- InputEvents.Init(this.EventManager);
- LocationEvents.Init(this.EventManager);
- MenuEvents.Init(this.EventManager);
- MineEvents.Init(this.EventManager);
- MultiplayerEvents.Init(this.EventManager);
- PlayerEvents.Init(this.EventManager);
- SaveEvents.Init(this.EventManager);
- SpecialisedEvents.Init(this.EventManager);
- TimeEvents.Init(this.EventManager);
+ ContentEvents.Init(this.EventManager, this.DeprecationManager);
+ ControlEvents.Init(this.EventManager, this.DeprecationManager);
+ GameEvents.Init(this.EventManager, this.DeprecationManager);
+ GraphicsEvents.Init(this.EventManager, this.DeprecationManager);
+ InputEvents.Init(this.EventManager, this.DeprecationManager);
+ LocationEvents.Init(this.EventManager, this.DeprecationManager);
+ MenuEvents.Init(this.EventManager, this.DeprecationManager);
+ MineEvents.Init(this.EventManager, this.DeprecationManager);
+ MultiplayerEvents.Init(this.EventManager, this.DeprecationManager);
+ PlayerEvents.Init(this.EventManager, this.DeprecationManager);
+ SaveEvents.Init(this.EventManager, this.DeprecationManager);
+ SpecialisedEvents.Init(this.EventManager, this.DeprecationManager);
+ TimeEvents.Init(this.EventManager, this.DeprecationManager);
+#endif
// init JSON parser
JsonConverter[] converters = {
@@ -216,7 +232,7 @@ namespace StardewModdingAPI.Framework
// override game
SGame.ConstructorHack = new SGameConstructorHack(this.Monitor, this.Reflection, this.Toolkit.JsonHelper);
- this.GameInstance = new SGame(this.Monitor, this.MonitorForGame, this.Reflection, this.EventManager, this.Toolkit.JsonHelper, this.ModRegistry, this.DeprecationManager, this.InitialiseAfterGameStart, this.Dispose);
+ this.GameInstance = new SGame(this.Monitor, this.MonitorForGame, this.Reflection, this.EventManager, this.Toolkit.JsonHelper, this.ModRegistry, this.DeprecationManager, this.OnLocaleChanged, this.InitialiseAfterGameStart, this.Dispose);
StardewValley.Program.gamePtr = this.GameInstance;
// add exit handler
@@ -239,12 +255,13 @@ namespace StardewModdingAPI.Framework
}
}).Start();
- // hook into game events
- ContentEvents.AfterLocaleChanged += (sender, e) => this.OnLocaleChanged();
-
// set window titles
this.GameInstance.Window.Title = $"Stardew Valley {Constants.GameVersion} - running SMAPI {Constants.ApiVersion}";
Console.Title = $"SMAPI {Constants.ApiVersion} - running Stardew Valley {Constants.GameVersion}";
+#if SMAPI_3_0_STRICT
+ this.GameInstance.Window.Title += " [SMAPI 3.0 strict mode]";
+ Console.Title += " [SMAPI 3.0 strict mode]";
+#endif
}
catch (Exception ex)
{
@@ -348,8 +365,11 @@ namespace StardewModdingAPI.Framework
private void InitialiseAfterGameStart()
{
// add headers
+#if SMAPI_3_0_STRICT
+ this.Monitor.Log($"You're running SMAPI 3.0 strict mode, so most mods won't work correctly. If that wasn't intended, install the normal version of SMAPI from https://smapi.io instead.", LogLevel.Warn);
+#endif
if (this.Settings.DeveloperMode)
- this.Monitor.Log($"You configured SMAPI to run in developer mode. The console may be much more verbose. You can disable developer mode by installing the non-developer version of SMAPI, or by editing {Constants.ApiConfigPath}.", LogLevel.Info);
+ this.Monitor.Log($"You have SMAPI for developers, so the console will be much more verbose. You can disable developer mode by installing the non-developer version of SMAPI, or by editing {Constants.ApiConfigPath}.", LogLevel.Info);
if (!this.Settings.CheckForUpdates)
this.Monitor.Log($"You configured SMAPI to not check for updates. Running an old version of SMAPI is not recommended. You can enable update checks by reinstalling SMAPI or editing {Constants.ApiConfigPath}.", LogLevel.Warn);
if (!this.Monitor.WriteToConsole)
@@ -409,6 +429,11 @@ namespace StardewModdingAPI.Framework
int modsLoaded = this.ModRegistry.GetAll().Count();
this.GameInstance.Window.Title = $"Stardew Valley {Constants.GameVersion} - running SMAPI {Constants.ApiVersion} with {modsLoaded} mods";
Console.Title = $"SMAPI {Constants.ApiVersion} - running Stardew Valley {Constants.GameVersion} with {modsLoaded} mods";
+#if SMAPI_3_0_STRICT
+ this.GameInstance.Window.Title += " [SMAPI 3.0 strict mode]";
+ Console.Title += " [SMAPI 3.0 strict mode]";
+#endif
+
// start SMAPI console
new Thread(this.RunConsoleLoop).Start();
@@ -701,7 +726,7 @@ namespace StardewModdingAPI.Framework
// load mods
IDictionary<IModMetadata, Tuple<string, string>> skippedMods = new Dictionary<IModMetadata, Tuple<string, string>>();
- using (AssemblyLoader modAssemblyLoader = new AssemblyLoader(Constants.Platform, this.Monitor))
+ using (AssemblyLoader modAssemblyLoader = new AssemblyLoader(Constants.Platform, this.Monitor, this.Settings.ParanoidWarnings))
{
// init
HashSet<string> suppressUpdateChecks = new HashSet<string>(this.Settings.SuppressUpdateChecks, StringComparer.InvariantCultureIgnoreCase);
@@ -891,11 +916,13 @@ namespace StardewModdingAPI.Framework
return false;
}
+#if !SMAPI_3_0_STRICT
// add deprecation warning for old version format
{
if (mod.Manifest?.Version is Toolkit.SemanticVersion version && version.IsLegacyFormat)
this.DeprecationManager.Warn(mod.DisplayName, "non-string manifest version", "2.8", DeprecationLevel.Notice);
}
+#endif
// validate dependencies
// Although dependences are validated before mods are loaded, a dependency may have failed to load.
@@ -1262,9 +1289,9 @@ namespace StardewModdingAPI.Framework
}
/// <summary>Redirect messages logged directly to the console to the given monitor.</summary>
- /// <param name="monitor">The monitor with which to log messages.</param>
+ /// <param name="gameMonitor">The monitor with which to log messages as the game.</param>
/// <param name="message">The message to log.</param>
- private void HandleConsoleMessage(IMonitor monitor, string message)
+ private void HandleConsoleMessage(IMonitor gameMonitor, string message)
{
// detect exception
LogLevel level = message.Contains("Exception") ? LogLevel.Error : LogLevel.Trace;
@@ -1273,8 +1300,19 @@ namespace StardewModdingAPI.Framework
if (level != LogLevel.Error && this.SuppressConsolePatterns.Any(p => p.IsMatch(message)))
return;
+ // show friendly error if applicable
+ foreach (var entry in this.ReplaceConsolePatterns)
+ {
+ if (entry.Item1.IsMatch(message))
+ {
+ this.Monitor.Log(entry.Item2, entry.Item3);
+ gameMonitor.Log(message, LogLevel.Trace);
+ return;
+ }
+ }
+
// forward to monitor
- monitor.Log(message, level);
+ gameMonitor.Log(message, level);
}
/// <summary>Show a 'press any key to exit' message, and exit when they press a key.</summary>
diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs
index 75cf4c52..7b3335b7 100644
--- a/src/SMAPI/Framework/SGame.cs
+++ b/src/SMAPI/Framework/SGame.cs
@@ -9,7 +9,9 @@ using System.Threading;
using System.Threading.Tasks;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
+#if !SMAPI_3_0_STRICT
using Microsoft.Xna.Framework.Input;
+#endif
using Netcode;
using StardewModdingAPI.Enums;
using StardewModdingAPI.Events;
@@ -29,7 +31,7 @@ using StardewValley.TerrainFeatures;
using StardewValley.Tools;
using xTile.Dimensions;
using xTile.Layers;
-using Object = StardewValley.Object;
+using SObject = StardewValley.Object;
namespace StardewModdingAPI.Framework
{
@@ -70,12 +72,15 @@ namespace StardewModdingAPI.Framework
/// <summary>Whether the after-load events were raised for this session.</summary>
private bool RaisedAfterLoadEvent;
- /// <summary>Whether the game is saving and SMAPI has already raised <see cref="SaveEvents.BeforeSave"/>.</summary>
+ /// <summary>Whether the game is saving and SMAPI has already raised <see cref="IGameLoopEvents.Saving"/>.</summary>
private bool IsBetweenSaveEvents;
- /// <summary>Whether the game is creating the save file and SMAPI has already raised <see cref="SaveEvents.BeforeCreate"/>.</summary>
+ /// <summary>Whether the game is creating the save file and SMAPI has already raised <see cref="IGameLoopEvents.SaveCreating"/>.</summary>
private bool IsBetweenCreateEvents;
+ /// <summary>A callback to invoke after the content language changes.</summary>
+ private readonly Action OnLocaleChanged;
+
/// <summary>A callback to invoke after the game finishes initialising.</summary>
private readonly Action OnGameInitialised;
@@ -138,9 +143,10 @@ namespace StardewModdingAPI.Framework
/// <param name="jsonHelper">Encapsulates SMAPI's JSON file parsing.</param>
/// <param name="modRegistry">Tracks the installed mods.</param>
/// <param name="deprecationManager">Manages deprecation warnings.</param>
+ /// <param name="onLocaleChanged">A callback to invoke after the content language changes.</param>
/// <param name="onGameInitialised">A callback to invoke after the game finishes initialising.</param>
/// <param name="onGameExiting">A callback to invoke when the game exits.</param>
- internal SGame(IMonitor monitor, IMonitor monitorForGame, Reflector reflection, EventManager eventManager, JsonHelper jsonHelper, ModRegistry modRegistry, DeprecationManager deprecationManager, Action onGameInitialised, Action onGameExiting)
+ internal SGame(IMonitor monitor, IMonitor monitorForGame, Reflector reflection, EventManager eventManager, JsonHelper jsonHelper, ModRegistry modRegistry, DeprecationManager deprecationManager, Action onLocaleChanged, Action onGameInitialised, Action onGameExiting)
{
SGame.ConstructorHack = null;
@@ -158,6 +164,7 @@ namespace StardewModdingAPI.Framework
this.ModRegistry = modRegistry;
this.Reflection = reflection;
this.DeprecationManager = deprecationManager;
+ this.OnLocaleChanged = onLocaleChanged;
this.OnGameInitialised = onGameInitialised;
this.OnGameExiting = onGameExiting;
Game1.input = new SInputState();
@@ -206,6 +213,17 @@ namespace StardewModdingAPI.Framework
this.Events.ModMessageReceived.RaiseForMods(new ModMessageReceivedEventArgs(message), mod => mod != null && modIDs.Contains(mod.Manifest.UniqueID));
}
+ /// <summary>A callback raised when the player quits a save and returns to the title screen.</summary>
+ private void OnReturnedToTitle()
+ {
+ this.Monitor.Log("Context: returned to title", LogLevel.Trace);
+ this.Multiplayer.Peers.Clear();
+ this.Events.ReturnedToTitle.RaiseEmpty();
+#if !SMAPI_3_0_STRICT
+ this.Events.Legacy_AfterReturnToTitle.Raise();
+#endif
+ }
+
/// <summary>Constructor a content manager to read XNB files.</summary>
/// <param name="serviceProvider">The service provider to use to locate services.</param>
/// <param name="rootDirectory">The root directory to search for content.</param>
@@ -287,7 +305,9 @@ namespace StardewModdingAPI.Framework
this.Events.UnvalidatedUpdateTicking.Raise(new UnvalidatedUpdateTickingEventArgs(this.TicksElapsed));
base.Update(gameTime);
this.Events.UnvalidatedUpdateTicked.Raise(new UnvalidatedUpdateTickedEventArgs(this.TicksElapsed));
+#if !SMAPI_3_0_STRICT
this.Events.Legacy_UnvalidatedUpdateTick.Raise();
+#endif
return;
}
@@ -334,7 +354,9 @@ namespace StardewModdingAPI.Framework
// This should *always* run, even when suppressing mod events, since the game uses
// this too. For example, doing this after mod event suppression would prevent the
// user from doing anything on the overnight shipping screen.
+#if !SMAPI_3_0_STRICT
SInputState previousInputState = this.Input.Clone();
+#endif
SInputState inputState = this.Input;
if (this.IsActive)
inputState.TrueUpdate();
@@ -355,7 +377,9 @@ namespace StardewModdingAPI.Framework
this.IsBetweenCreateEvents = true;
this.Monitor.Log("Context: before save creation.", LogLevel.Trace);
this.Events.SaveCreating.RaiseEmpty();
+#if !SMAPI_3_0_STRICT
this.Events.Legacy_BeforeCreateSave.Raise();
+#endif
}
// raise before-save
@@ -364,14 +388,18 @@ namespace StardewModdingAPI.Framework
this.IsBetweenSaveEvents = true;
this.Monitor.Log("Context: before save.", LogLevel.Trace);
this.Events.Saving.RaiseEmpty();
+#if !SMAPI_3_0_STRICT
this.Events.Legacy_BeforeSave.Raise();
+#endif
}
// suppress non-save events
this.Events.UnvalidatedUpdateTicking.Raise(new UnvalidatedUpdateTickingEventArgs(this.TicksElapsed));
base.Update(gameTime);
this.Events.UnvalidatedUpdateTicked.Raise(new UnvalidatedUpdateTickedEventArgs(this.TicksElapsed));
+#if !SMAPI_3_0_STRICT
this.Events.Legacy_UnvalidatedUpdateTick.Raise();
+#endif
return;
}
if (this.IsBetweenCreateEvents)
@@ -380,7 +408,9 @@ namespace StardewModdingAPI.Framework
this.IsBetweenCreateEvents = false;
this.Monitor.Log($"Context: after save creation, starting {Game1.currentSeason} {Game1.dayOfMonth} Y{Game1.year}.", LogLevel.Trace);
this.Events.SaveCreated.RaiseEmpty();
+#if !SMAPI_3_0_STRICT
this.Events.Legacy_AfterCreateSave.Raise();
+#endif
}
if (this.IsBetweenSaveEvents)
{
@@ -389,9 +419,10 @@ namespace StardewModdingAPI.Framework
this.Monitor.Log($"Context: after save, starting {Game1.currentSeason} {Game1.dayOfMonth} Y{Game1.year}.", LogLevel.Trace);
this.Events.Saved.RaiseEmpty();
this.Events.DayStarted.RaiseEmpty();
-
+#if !SMAPI_3_0_STRICT
this.Events.Legacy_AfterSave.Raise();
this.Events.Legacy_AfterDayStarted.Raise();
+#endif
}
/*********
@@ -421,7 +452,11 @@ namespace StardewModdingAPI.Framework
var now = this.Watchers.LocaleWatcher.CurrentValue;
this.Monitor.Log($"Context: locale set to {now}.", LogLevel.Trace);
+
+ this.OnLocaleChanged();
+#if !SMAPI_3_0_STRICT
this.Events.Legacy_LocaleChanged.Raise(new EventArgsValueChanged<string>(was.ToString(), now.ToString()));
+#endif
this.Watchers.LocaleWatcher.Reset();
}
@@ -430,11 +465,7 @@ namespace StardewModdingAPI.Framework
** Load / return-to-title events
*********/
if (wasWorldReady && !Context.IsWorldReady)
- {
- this.Monitor.Log("Context: returned to title", LogLevel.Trace);
- this.Events.ReturnedToTitle.RaiseEmpty();
- this.Events.Legacy_AfterReturnToTitle.Raise();
- }
+ this.OnReturnedToTitle();
else if (!this.RaisedAfterLoadEvent && Context.IsWorldReady)
{
// print context
@@ -452,9 +483,10 @@ namespace StardewModdingAPI.Framework
this.RaisedAfterLoadEvent = true;
this.Events.SaveLoaded.RaiseEmpty();
this.Events.DayStarted.RaiseEmpty();
-
+#if !SMAPI_3_0_STRICT
this.Events.Legacy_AfterLoad.Raise();
this.Events.Legacy_AfterDayStarted.Raise();
+#endif
}
/*********
@@ -473,7 +505,9 @@ namespace StardewModdingAPI.Framework
Point newSize = this.Watchers.WindowSizeWatcher.CurrentValue;
this.Events.WindowResized.Raise(new WindowResizedEventArgs(oldSize, newSize));
+#if !SMAPI_3_0_STRICT
this.Events.Legacy_Resize.Raise();
+#endif
this.Watchers.WindowSizeWatcher.Reset();
}
@@ -522,9 +556,10 @@ namespace StardewModdingAPI.Framework
this.Monitor.Log($"Events: button {button} pressed.", LogLevel.Trace);
this.Events.ButtonPressed.Raise(new ButtonPressedEventArgs(button, cursor, inputState));
- this.Events.Legacy_ButtonPressed.Raise(new EventArgsInput(button, cursor, inputState.SuppressButtons));
+#if !SMAPI_3_0_STRICT
// legacy events
+ this.Events.Legacy_ButtonPressed.Raise(new EventArgsInput(button, cursor, inputState.SuppressButtons));
if (button.TryGetKeyboard(out Keys key))
{
if (key != Keys.None)
@@ -537,6 +572,7 @@ namespace StardewModdingAPI.Framework
else
this.Events.Legacy_ControllerButtonPressed.Raise(new EventArgsControllerButtonPressed(PlayerIndex.One, controllerButton));
}
+#endif
}
else if (status == InputStatus.Released)
{
@@ -544,9 +580,10 @@ namespace StardewModdingAPI.Framework
this.Monitor.Log($"Events: button {button} released.", LogLevel.Trace);
this.Events.ButtonReleased.Raise(new ButtonReleasedEventArgs(button, cursor, inputState));
- this.Events.Legacy_ButtonReleased.Raise(new EventArgsInput(button, cursor, inputState.SuppressButtons));
+#if !SMAPI_3_0_STRICT
// legacy events
+ this.Events.Legacy_ButtonReleased.Raise(new EventArgsInput(button, cursor, inputState.SuppressButtons));
if (button.TryGetKeyboard(out Keys key))
{
if (key != Keys.None)
@@ -559,14 +596,17 @@ namespace StardewModdingAPI.Framework
else
this.Events.Legacy_ControllerButtonReleased.Raise(new EventArgsControllerButtonReleased(PlayerIndex.One, controllerButton));
}
+#endif
}
}
+#if !SMAPI_3_0_STRICT
// raise legacy state-changed events
if (inputState.RealKeyboard != previousInputState.RealKeyboard)
this.Events.Legacy_KeyboardChanged.Raise(new EventArgsKeyboardStateChanged(previousInputState.RealKeyboard, inputState.RealKeyboard));
if (inputState.RealMouse != previousInputState.RealMouse)
this.Events.Legacy_MouseChanged.Raise(new EventArgsMouseStateChanged(previousInputState.RealMouse, inputState.RealMouse, new Point((int)previousInputState.CursorPosition.ScreenPixels.X, (int)previousInputState.CursorPosition.ScreenPixels.Y), new Point((int)inputState.CursorPosition.ScreenPixels.X, (int)inputState.CursorPosition.ScreenPixels.Y)));
+#endif
}
}
@@ -584,10 +624,12 @@ namespace StardewModdingAPI.Framework
// raise menu events
this.Events.MenuChanged.Raise(new MenuChangedEventArgs(was, now));
+#if !SMAPI_3_0_STRICT
if (now != null)
this.Events.Legacy_MenuChanged.Raise(new EventArgsClickableMenuChanged(was, now));
else
this.Events.Legacy_MenuClosed.Raise(new EventArgsClickableMenuClosed(was));
+#endif
}
/*********
@@ -615,7 +657,9 @@ namespace StardewModdingAPI.Framework
}
this.Events.LocationListChanged.Raise(new LocationListChangedEventArgs(added, removed));
+#if !SMAPI_3_0_STRICT
this.Events.Legacy_LocationsChanged.Raise(new EventArgsLocationsChanged(added, removed));
+#endif
}
// raise location contents changed
@@ -632,7 +676,9 @@ namespace StardewModdingAPI.Framework
watcher.BuildingsWatcher.Reset();
this.Events.BuildingListChanged.Raise(new BuildingListChangedEventArgs(location, added, removed));
+#if !SMAPI_3_0_STRICT
this.Events.Legacy_BuildingsChanged.Raise(new EventArgsLocationBuildingsChanged(location, added, removed));
+#endif
}
// debris changed
@@ -672,12 +718,14 @@ namespace StardewModdingAPI.Framework
if (watcher.ObjectsWatcher.IsChanged)
{
GameLocation location = watcher.Location;
- KeyValuePair<Vector2, Object>[] added = watcher.ObjectsWatcher.Added.ToArray();
- KeyValuePair<Vector2, Object>[] removed = watcher.ObjectsWatcher.Removed.ToArray();
+ KeyValuePair<Vector2, SObject>[] added = watcher.ObjectsWatcher.Added.ToArray();
+ KeyValuePair<Vector2, SObject>[] removed = watcher.ObjectsWatcher.Removed.ToArray();
watcher.ObjectsWatcher.Reset();
this.Events.ObjectListChanged.Raise(new ObjectListChangedEventArgs(location, added, removed));
+#if !SMAPI_3_0_STRICT
this.Events.Legacy_ObjectsChanged.Raise(new EventArgsLocationObjectsChanged(location, added, removed));
+#endif
}
// terrain features changed
@@ -707,7 +755,9 @@ namespace StardewModdingAPI.Framework
this.Monitor.Log($"Events: time changed from {was} to {now}.", LogLevel.Trace);
this.Events.TimeChanged.Raise(new TimeChangedEventArgs(was, now));
+#if !SMAPI_3_0_STRICT
this.Events.Legacy_TimeOfDayChanged.Raise(new EventArgsIntChanged(was, now));
+#endif
}
else
this.Watchers.TimeWatcher.Reset();
@@ -725,7 +775,9 @@ namespace StardewModdingAPI.Framework
GameLocation oldLocation = playerTracker.LocationWatcher.PreviousValue;
this.Events.Warped.Raise(new WarpedEventArgs(playerTracker.Player, oldLocation, newLocation));
+#if !SMAPI_3_0_STRICT
this.Events.Legacy_PlayerWarped.Raise(new EventArgsPlayerWarped(oldLocation, newLocation));
+#endif
}
// raise player leveled up a skill
@@ -735,7 +787,9 @@ namespace StardewModdingAPI.Framework
this.Monitor.Log($"Events: player skill '{pair.Key}' changed from {pair.Value.PreviousValue} to {pair.Value.CurrentValue}.", LogLevel.Trace);
this.Events.LevelChanged.Raise(new LevelChangedEventArgs(playerTracker.Player, pair.Key, pair.Value.PreviousValue, pair.Value.CurrentValue));
+#if !SMAPI_3_0_STRICT
this.Events.Legacy_LeveledUp.Raise(new EventArgsLevelUp((EventArgsLevelUp.LevelType)pair.Key, pair.Value.CurrentValue));
+#endif
}
// raise player inventory changed
@@ -745,7 +799,9 @@ namespace StardewModdingAPI.Framework
if (this.Monitor.IsVerbose)
this.Monitor.Log("Events: player inventory changed.", LogLevel.Trace);
this.Events.InventoryChanged.Raise(new InventoryChangedEventArgs(playerTracker.Player, changedItems));
+#if !SMAPI_3_0_STRICT
this.Events.Legacy_InventoryChanged.Raise(new EventArgsInventoryChanged(Game1.player.Items, changedItems));
+#endif
}
// raise mine level changed
@@ -753,7 +809,9 @@ namespace StardewModdingAPI.Framework
{
if (this.Monitor.IsVerbose)
this.Monitor.Log($"Context: mine level changed to {mineLevel}.", LogLevel.Trace);
+#if !SMAPI_3_0_STRICT
this.Events.Legacy_MineLevelChanged.Raise(new EventArgsMineLevelChanged(playerTracker.MineLevelWatcher.PreviousValue, mineLevel));
+#endif
}
}
this.Watchers.CurrentPlayerTracker?.Reset();
@@ -785,6 +843,7 @@ namespace StardewModdingAPI.Framework
/*********
** Update events
*********/
+#if !SMAPI_3_0_STRICT
this.Events.Legacy_UnvalidatedUpdateTick.Raise();
if (this.TicksElapsed == 1)
this.Events.Legacy_FirstUpdateTick.Raise();
@@ -801,6 +860,7 @@ namespace StardewModdingAPI.Framework
this.Events.Legacy_HalfSecondTick.Raise();
if (this.CurrentUpdateTick % 60 == 0)
this.Events.Legacy_OneSecondTick.Raise();
+#endif
this.CurrentUpdateTick += 1;
if (this.CurrentUpdateTick >= 60)
this.CurrentUpdateTick = 0;
@@ -890,10 +950,14 @@ namespace StardewModdingAPI.Framework
try
{
this.Events.RenderingActiveMenu.RaiseEmpty();
+#if !SMAPI_3_0_STRICT
this.Events.Legacy_OnPreRenderGuiEvent.Raise();
+#endif
activeClickableMenu.draw(Game1.spriteBatch);
this.Events.RenderedActiveMenu.RaiseEmpty();
+#if !SMAPI_3_0_STRICT
this.Events.Legacy_OnPostRenderGuiEvent.Raise();
+#endif
}
catch (Exception ex)
{
@@ -901,7 +965,9 @@ namespace StardewModdingAPI.Framework
activeClickableMenu.exitThisMenu();
}
this.Events.Rendered.RaiseEmpty();
+#if !SMAPI_3_0_STRICT
this.Events.Legacy_OnPostRenderEvent.Raise();
+#endif
Game1.spriteBatch.End();
}
@@ -925,10 +991,14 @@ namespace StardewModdingAPI.Framework
{
Game1.activeClickableMenu.drawBackground(Game1.spriteBatch);
this.Events.RenderingActiveMenu.RaiseEmpty();
+#if !SMAPI_3_0_STRICT
this.Events.Legacy_OnPreRenderGuiEvent.Raise();
+#endif
Game1.activeClickableMenu.draw(Game1.spriteBatch);
this.Events.RenderedActiveMenu.RaiseEmpty();
+#if !SMAPI_3_0_STRICT
this.Events.Legacy_OnPostRenderGuiEvent.Raise();
+#endif
}
catch (Exception ex)
{
@@ -936,7 +1006,9 @@ namespace StardewModdingAPI.Framework
Game1.activeClickableMenu.exitThisMenu();
}
this.Events.Rendered.RaiseEmpty();
+#if !SMAPI_3_0_STRICT
this.Events.Legacy_OnPostRenderEvent.Raise();
+#endif
Game1.spriteBatch.End();
this.drawOverlays(Game1.spriteBatch);
if ((double)Game1.options.zoomLevel != 1.0)
@@ -961,7 +1033,9 @@ namespace StardewModdingAPI.Framework
Game1.spriteBatch.DrawString(Game1.dialogueFont, Game1.content.LoadString("Strings\\StringsFromCSFiles:Game1.cs.3686"), new Vector2(16f, 32f), new Color(0, (int)byte.MaxValue, 0));
Game1.spriteBatch.DrawString(Game1.dialogueFont, Game1.parseText(Game1.errorMessage, Game1.dialogueFont, Game1.graphics.GraphicsDevice.Viewport.Width), new Vector2(16f, 48f), Color.White);
this.Events.Rendered.RaiseEmpty();
+#if !SMAPI_3_0_STRICT
this.Events.Legacy_OnPostRenderEvent.Raise();
+#endif
Game1.spriteBatch.End();
}
else if (Game1.currentMinigame != null)
@@ -974,7 +1048,9 @@ namespace StardewModdingAPI.Framework
Game1.spriteBatch.End();
}
this.drawOverlays(Game1.spriteBatch);
+#if !SMAPI_3_0_STRICT
this.RaisePostRender(needsNewBatch: true);
+#endif
if ((double)Game1.options.zoomLevel == 1.0)
return;
this.GraphicsDevice.SetRenderTarget((RenderTarget2D)null);
@@ -992,10 +1068,14 @@ namespace StardewModdingAPI.Framework
try
{
this.Events.RenderingActiveMenu.RaiseEmpty();
+#if !SMAPI_3_0_STRICT
this.Events.Legacy_OnPreRenderGuiEvent.Raise();
+#endif
Game1.activeClickableMenu.draw(Game1.spriteBatch);
this.Events.RenderedActiveMenu.RaiseEmpty();
+#if !SMAPI_3_0_STRICT
this.Events.Legacy_OnPostRenderGuiEvent.Raise();
+#endif
}
catch (Exception ex)
{
@@ -1085,7 +1165,9 @@ namespace StardewModdingAPI.Framework
if (++batchOpens == 1)
this.Events.Rendering.RaiseEmpty();
this.Events.RenderingWorld.RaiseEmpty();
+#if !SMAPI_3_0_STRICT
this.Events.Legacy_OnPreRenderEvent.Raise();
+#endif
if (Game1.background != null)
Game1.background.draw(Game1.spriteBatch);
Game1.mapDisplayDevice.BeginScene(Game1.spriteBatch);
@@ -1340,6 +1422,7 @@ namespace StardewModdingAPI.Framework
}
Game1.spriteBatch.End();
}
+ this.Events.RenderedWorld.RaiseEmpty();
Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null);
if (Game1.drawGrid)
{
@@ -1397,10 +1480,14 @@ namespace StardewModdingAPI.Framework
if ((Game1.displayHUD || Game1.eventUp) && (Game1.currentBillboard == 0 && Game1.gameMode == (byte)3) && (!Game1.freezeControls && !Game1.panMode && !Game1.HostPaused))
{
this.Events.RenderingHud.RaiseEmpty();
+#if !SMAPI_3_0_STRICT
this.Events.Legacy_OnPreRenderHudEvent.Raise();
+#endif
this.drawHUD();
this.Events.RenderedHud.RaiseEmpty();
+#if !SMAPI_3_0_STRICT
this.Events.Legacy_OnPostRenderHudEvent.Raise();
+#endif
}
else if (Game1.activeClickableMenu == null && Game1.farmEvent == null)
Game1.spriteBatch.Draw(Game1.mouseCursors, new Vector2((float)Game1.getOldMouseX(), (float)Game1.getOldMouseY()), new Microsoft.Xna.Framework.Rectangle?(Game1.getSourceRectForStandardTileSheet(Game1.mouseCursors, 0, 16, 16)), Color.White, 0.0f, Vector2.Zero, (float)(4.0 + (double)Game1.dialogueButtonScale / 150.0), SpriteEffects.None, 1f);
@@ -1509,10 +1596,14 @@ namespace StardewModdingAPI.Framework
try
{
this.Events.RenderingActiveMenu.RaiseEmpty();
+#if !SMAPI_3_0_STRICT
this.Events.Legacy_OnPreRenderGuiEvent.Raise();
+#endif
Game1.activeClickableMenu.draw(Game1.spriteBatch);
this.Events.RenderedActiveMenu.RaiseEmpty();
+#if !SMAPI_3_0_STRICT
this.Events.Legacy_OnPostRenderGuiEvent.Raise();
+#endif
}
catch (Exception ex)
{
@@ -1527,9 +1618,11 @@ namespace StardewModdingAPI.Framework
string s = Game1.content.LoadString("Strings\\StringsFromCSFiles:DayTimeMoneyBox.cs.10378");
SpriteText.drawStringWithScrollBackground(Game1.spriteBatch, s, 96, 32, "", 1f, -1);
}
- this.Events.RenderedWorld.RaiseEmpty();
+
this.Events.Rendered.RaiseEmpty();
+#if !SMAPI_3_0_STRICT
this.Events.Legacy_OnPostRenderEvent.Raise();
+#endif
Game1.spriteBatch.End();
this.drawOverlays(Game1.spriteBatch);
this.renderScreenBuffer();
@@ -1549,6 +1642,7 @@ namespace StardewModdingAPI.Framework
this.RaisedAfterLoadEvent = false;
}
+#if !SMAPI_3_0_STRICT
/// <summary>Raise the <see cref="GraphicsEvents.OnPostRenderEvent"/> if there are any listeners.</summary>
/// <param name="needsNewBatch">Whether to create a new sprite batch.</param>
private void RaisePostRender(bool needsNewBatch = false)
@@ -1562,5 +1656,6 @@ namespace StardewModdingAPI.Framework
Game1.spriteBatch.End();
}
}
+#endif
}
}
diff --git a/src/SMAPI/Framework/SMultiplayer.cs b/src/SMAPI/Framework/SMultiplayer.cs
index 629fce1d..12cd2d46 100644
--- a/src/SMAPI/Framework/SMultiplayer.cs
+++ b/src/SMAPI/Framework/SMultiplayer.cs
@@ -82,6 +82,7 @@ namespace StardewModdingAPI.Framework
this.OnModMessageReceived = onModMessageReceived;
}
+#if !SMAPI_3_0_STRICT
/// <summary>Handle sync messages from other players and perform other initial sync logic.</summary>
public override void UpdateEarly()
{
@@ -97,6 +98,7 @@ namespace StardewModdingAPI.Framework
base.UpdateLate(forceSync);
this.EventManager.Legacy_AfterMainBroadcast.Raise();
}
+#endif
/// <summary>Initialise a client before the game connects to a remote server.</summary>
/// <param name="client">The client to initialise.</param>
diff --git a/src/SMAPI/Framework/StateTracking/FieldWatchers/ComparableListWatcher.cs b/src/SMAPI/Framework/StateTracking/FieldWatchers/ComparableListWatcher.cs
new file mode 100644
index 00000000..2ea6609a
--- /dev/null
+++ b/src/SMAPI/Framework/StateTracking/FieldWatchers/ComparableListWatcher.cs
@@ -0,0 +1,83 @@
+using System.Collections.Generic;
+using System.Linq;
+
+namespace StardewModdingAPI.Framework.StateTracking.FieldWatchers
+{
+ /// <summary>A watcher which detects changes to a collection of values using a specified <see cref="IEqualityComparer{T}"/> instance.</summary>
+ /// <typeparam name="TValue">The value type within the collection.</typeparam>
+ internal class ComparableListWatcher<TValue> : BaseDisposableWatcher, ICollectionWatcher<TValue>
+ {
+ /*********
+ ** Properties
+ *********/
+ /// <summary>The collection to watch.</summary>
+ private readonly ICollection<TValue> CurrentValues;
+
+ /// <summary>The values during the previous update.</summary>
+ private HashSet<TValue> LastValues;
+
+ /// <summary>The pairs added since the last reset.</summary>
+ private readonly List<TValue> AddedImpl = new List<TValue>();
+
+ /// <summary>The pairs removed since the last reset.</summary>
+ private readonly List<TValue> RemovedImpl = new List<TValue>();
+
+
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>Whether the value changed since the last reset.</summary>
+ public bool IsChanged => this.AddedImpl.Count > 0 || this.RemovedImpl.Count > 0;
+
+ /// <summary>The values added since the last reset.</summary>
+ public IEnumerable<TValue> Added => this.AddedImpl;
+
+ /// <summary>The values removed since the last reset.</summary>
+ public IEnumerable<TValue> Removed => this.RemovedImpl;
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="values">The collection to watch.</param>
+ /// <param name="comparer">The equality comparer which indicates whether two values are the same.</param>
+ public ComparableListWatcher(ICollection<TValue> values, IEqualityComparer<TValue> comparer)
+ {
+ this.CurrentValues = values;
+ this.LastValues = new HashSet<TValue>(comparer);
+ }
+
+ /// <summary>Update the current value if needed.</summary>
+ public void Update()
+ {
+ this.AssertNotDisposed();
+
+ // optimise for zero items
+ if (this.CurrentValues.Count == 0)
+ {
+ if (this.LastValues.Count > 0)
+ {
+ this.AddedImpl.AddRange(this.LastValues);
+ this.LastValues.Clear();
+ }
+ return;
+ }
+
+ // detect changes
+ HashSet<TValue> curValues = new HashSet<TValue>(this.CurrentValues, this.LastValues.Comparer);
+ this.RemovedImpl.AddRange(from value in this.LastValues where !curValues.Contains(value) select value);
+ this.AddedImpl.AddRange(from value in curValues where !this.LastValues.Contains(value) select value);
+ this.LastValues = curValues;
+ }
+
+ /// <summary>Set the current value as the baseline.</summary>
+ public void Reset()
+ {
+ this.AssertNotDisposed();
+
+ this.AddedImpl.Clear();
+ this.RemovedImpl.Clear();
+ }
+ }
+}
diff --git a/src/SMAPI/Framework/StateTracking/FieldWatchers/ComparableWatcher.cs b/src/SMAPI/Framework/StateTracking/FieldWatchers/ComparableWatcher.cs
index d51fc2ac..dda30a15 100644
--- a/src/SMAPI/Framework/StateTracking/FieldWatchers/ComparableWatcher.cs
+++ b/src/SMAPI/Framework/StateTracking/FieldWatchers/ComparableWatcher.cs
@@ -4,26 +4,27 @@ using System.Collections.Generic;
namespace StardewModdingAPI.Framework.StateTracking.FieldWatchers
{
/// <summary>A watcher which detects changes to a value using a specified <see cref="IEqualityComparer{T}"/> instance.</summary>
- internal class ComparableWatcher<T> : IValueWatcher<T>
+ /// <typeparam name="TValue">The comparable value type.</typeparam>
+ internal class ComparableWatcher<TValue> : IValueWatcher<TValue>
{
/*********
** Properties
*********/
/// <summary>Get the current value.</summary>
- private readonly Func<T> GetValue;
+ private readonly Func<TValue> GetValue;
/// <summary>The equality comparer.</summary>
- private readonly IEqualityComparer<T> Comparer;
+ private readonly IEqualityComparer<TValue> Comparer;
/*********
** Accessors
*********/
/// <summary>The field value at the last reset.</summary>
- public T PreviousValue { get; private set; }
+ public TValue PreviousValue { get; private set; }
/// <summary>The latest value.</summary>
- public T CurrentValue { get; private set; }
+ public TValue CurrentValue { get; private set; }
/// <summary>Whether the value changed since the last reset.</summary>
public bool IsChanged { get; private set; }
@@ -35,7 +36,7 @@ namespace StardewModdingAPI.Framework.StateTracking.FieldWatchers
/// <summary>Construct an instance.</summary>
/// <param name="getValue">Get the current value.</param>
/// <param name="comparer">The equality comparer which indicates whether two values are the same.</param>
- public ComparableWatcher(Func<T> getValue, IEqualityComparer<T> comparer)
+ public ComparableWatcher(Func<TValue> getValue, IEqualityComparer<TValue> comparer)
{
this.GetValue = getValue;
this.Comparer = comparer;
diff --git a/src/SMAPI/Framework/StateTracking/FieldWatchers/NetCollectionWatcher.cs b/src/SMAPI/Framework/StateTracking/FieldWatchers/NetCollectionWatcher.cs
index 8a841a79..d3022a69 100644
--- a/src/SMAPI/Framework/StateTracking/FieldWatchers/NetCollectionWatcher.cs
+++ b/src/SMAPI/Framework/StateTracking/FieldWatchers/NetCollectionWatcher.cs
@@ -4,6 +4,7 @@ using Netcode;
namespace StardewModdingAPI.Framework.StateTracking.FieldWatchers
{
/// <summary>A watcher which detects changes to a Netcode collection.</summary>
+ /// <typeparam name="TValue">The value type within the collection.</typeparam>
internal class NetCollectionWatcher<TValue> : BaseDisposableWatcher, ICollectionWatcher<TValue>
where TValue : class, INetObject<INetSerializable>
{
@@ -16,7 +17,7 @@ namespace StardewModdingAPI.Framework.StateTracking.FieldWatchers
/// <summary>The pairs added since the last reset.</summary>
private readonly List<TValue> AddedImpl = new List<TValue>();
- /// <summary>The pairs demoved since the last reset.</summary>
+ /// <summary>The pairs removed since the last reset.</summary>
private readonly List<TValue> RemovedImpl = new List<TValue>();
diff --git a/src/SMAPI/Framework/StateTracking/FieldWatchers/NetDictionaryWatcher.cs b/src/SMAPI/Framework/StateTracking/FieldWatchers/NetDictionaryWatcher.cs
index 7a2bf84e..7a7ab89d 100644
--- a/src/SMAPI/Framework/StateTracking/FieldWatchers/NetDictionaryWatcher.cs
+++ b/src/SMAPI/Framework/StateTracking/FieldWatchers/NetDictionaryWatcher.cs
@@ -20,7 +20,7 @@ namespace StardewModdingAPI.Framework.StateTracking.FieldWatchers
/// <summary>The pairs added since the last reset.</summary>
private readonly IDictionary<TKey, TValue> PairsAdded = new Dictionary<TKey, TValue>();
- /// <summary>The pairs demoved since the last reset.</summary>
+ /// <summary>The pairs removed since the last reset.</summary>
private readonly IDictionary<TKey, TValue> PairsRemoved = new Dictionary<TKey, TValue>();
/// <summary>The field being watched.</summary>
diff --git a/src/SMAPI/Framework/StateTracking/FieldWatchers/NetValueWatcher.cs b/src/SMAPI/Framework/StateTracking/FieldWatchers/NetValueWatcher.cs
index 188ed9f3..85099988 100644
--- a/src/SMAPI/Framework/StateTracking/FieldWatchers/NetValueWatcher.cs
+++ b/src/SMAPI/Framework/StateTracking/FieldWatchers/NetValueWatcher.cs
@@ -3,13 +3,15 @@ using Netcode;
namespace StardewModdingAPI.Framework.StateTracking.FieldWatchers
{
/// <summary>A watcher which detects changes to a net value field.</summary>
- internal class NetValueWatcher<T, TSelf> : BaseDisposableWatcher, IValueWatcher<T> where TSelf : NetFieldBase<T, TSelf>
+ /// <typeparam name="TValue">The value type wrapped by the net field.</typeparam>
+ /// <typeparam name="TNetField">The net field type.</typeparam>
+ internal class NetValueWatcher<TValue, TNetField> : BaseDisposableWatcher, IValueWatcher<TValue> where TNetField : NetFieldBase<TValue, TNetField>
{
/*********
** Properties
*********/
/// <summary>The field being watched.</summary>
- private readonly NetFieldBase<T, TSelf> Field;
+ private readonly NetFieldBase<TValue, TNetField> Field;
/*********
@@ -19,10 +21,10 @@ namespace StardewModdingAPI.Framework.StateTracking.FieldWatchers
public bool IsChanged { get; private set; }
/// <summary>The field value at the last reset.</summary>
- public T PreviousValue { get; private set; }
+ public TValue PreviousValue { get; private set; }
/// <summary>The latest value.</summary>
- public T CurrentValue { get; private set; }
+ public TValue CurrentValue { get; private set; }
/*********
@@ -30,7 +32,7 @@ namespace StardewModdingAPI.Framework.StateTracking.FieldWatchers
*********/
/// <summary>Construct an instance.</summary>
/// <param name="field">The field to watch.</param>
- public NetValueWatcher(NetFieldBase<T, TSelf> field)
+ public NetValueWatcher(NetFieldBase<TValue, TNetField> field)
{
this.Field = field;
this.PreviousValue = field.Value;
@@ -74,7 +76,7 @@ namespace StardewModdingAPI.Framework.StateTracking.FieldWatchers
/// <param name="field">The field being watched.</param>
/// <param name="oldValue">The old field value.</param>
/// <param name="newValue">The new field value.</param>
- private void OnValueChanged(TSelf field, T oldValue, T newValue)
+ private void OnValueChanged(TNetField field, TValue oldValue, TValue newValue)
{
this.CurrentValue = newValue;
this.IsChanged = true;
diff --git a/src/SMAPI/Framework/StateTracking/FieldWatchers/ObservableCollectionWatcher.cs b/src/SMAPI/Framework/StateTracking/FieldWatchers/ObservableCollectionWatcher.cs
index 34a97097..0c65789f 100644
--- a/src/SMAPI/Framework/StateTracking/FieldWatchers/ObservableCollectionWatcher.cs
+++ b/src/SMAPI/Framework/StateTracking/FieldWatchers/ObservableCollectionWatcher.cs
@@ -6,6 +6,7 @@ using System.Linq;
namespace StardewModdingAPI.Framework.StateTracking.FieldWatchers
{
/// <summary>A watcher which detects changes to an observable collection.</summary>
+ /// <typeparam name="TValue">The value type within the collection.</typeparam>
internal class ObservableCollectionWatcher<TValue> : BaseDisposableWatcher, ICollectionWatcher<TValue>
{
/*********
@@ -17,7 +18,7 @@ namespace StardewModdingAPI.Framework.StateTracking.FieldWatchers
/// <summary>The pairs added since the last reset.</summary>
private readonly List<TValue> AddedImpl = new List<TValue>();
- /// <summary>The pairs demoved since the last reset.</summary>
+ /// <summary>The pairs removed since the last reset.</summary>
private readonly List<TValue> RemovedImpl = new List<TValue>();
diff --git a/src/SMAPI/Framework/StateTracking/FieldWatchers/WatcherFactory.cs b/src/SMAPI/Framework/StateTracking/FieldWatchers/WatcherFactory.cs
index ab4ab0d5..8301351e 100644
--- a/src/SMAPI/Framework/StateTracking/FieldWatchers/WatcherFactory.cs
+++ b/src/SMAPI/Framework/StateTracking/FieldWatchers/WatcherFactory.cs
@@ -36,6 +36,14 @@ namespace StardewModdingAPI.Framework.StateTracking.FieldWatchers
return new ComparableWatcher<T>(getValue, new ObjectReferenceComparer<T>());
}
+ /// <summary>Get a watcher which detects when an object reference in a collection changes.</summary>
+ /// <typeparam name="T">The value type.</typeparam>
+ /// <param name="collection">The observable collection.</param>
+ public static ComparableListWatcher<T> ForReferenceList<T>(ICollection<T> collection)
+ {
+ return new ComparableListWatcher<T>(collection, new ObjectReferenceComparer<T>());
+ }
+
/// <summary>Get a watcher for an observable collection.</summary>
/// <typeparam name="T">The value type.</typeparam>
/// <param name="collection">The observable collection.</param>
diff --git a/src/SMAPI/Framework/StateTracking/WorldLocationsTracker.cs b/src/SMAPI/Framework/StateTracking/WorldLocationsTracker.cs
index 5a259663..d9d598f8 100644
--- a/src/SMAPI/Framework/StateTracking/WorldLocationsTracker.cs
+++ b/src/SMAPI/Framework/StateTracking/WorldLocationsTracker.cs
@@ -1,4 +1,3 @@
-using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
@@ -19,6 +18,9 @@ namespace StardewModdingAPI.Framework.StateTracking
/// <summary>Tracks changes to the location list.</summary>
private readonly ICollectionWatcher<GameLocation> LocationListWatcher;
+ /// <summary>Tracks changes to the list of active mine locations.</summary>
+ private readonly ICollectionWatcher<MineShaft> MineLocationListWatcher;
+
/// <summary>A lookup of the tracked locations.</summary>
private IDictionary<GameLocation, LocationTracker> LocationDict { get; } = new Dictionary<GameLocation, LocationTracker>(new ObjectReferenceComparer<GameLocation>());
@@ -50,24 +52,34 @@ namespace StardewModdingAPI.Framework.StateTracking
*********/
/// <summary>Construct an instance.</summary>
/// <param name="locations">The game's list of locations.</param>
- public WorldLocationsTracker(ObservableCollection<GameLocation> locations)
+ /// <param name="activeMineLocations">The game's list of active mine locations.</param>
+ public WorldLocationsTracker(ObservableCollection<GameLocation> locations, IList<MineShaft> activeMineLocations)
{
this.LocationListWatcher = WatcherFactory.ForObservableCollection(locations);
+ this.MineLocationListWatcher = WatcherFactory.ForReferenceList(activeMineLocations);
}
/// <summary>Update the current value if needed.</summary>
public void Update()
{
- // detect location changes
+ // detect added/removed locations
+ this.LocationListWatcher.Update();
+ this.MineLocationListWatcher.Update();
if (this.LocationListWatcher.IsChanged)
{
this.Remove(this.LocationListWatcher.Removed);
this.Add(this.LocationListWatcher.Added);
}
+ if (this.MineLocationListWatcher.IsChanged)
+ {
+ this.Remove(this.MineLocationListWatcher.Removed);
+ this.Add(this.MineLocationListWatcher.Added);
+ }
- // detect building changes
+ // detect building changed
foreach (LocationTracker watcher in this.Locations.ToArray())
{
+ watcher.Update();
if (watcher.BuildingsWatcher.IsChanged)
{
this.Remove(watcher.BuildingsWatcher.Removed);
@@ -75,7 +87,7 @@ namespace StardewModdingAPI.Framework.StateTracking
}
}
- // detect building interior changed (e.g. construction completed)
+ // detect building interiors changed (e.g. construction completed)
foreach (KeyValuePair<Building, GameLocation> pair in this.BuildingIndoors.Where(p => !object.Equals(p.Key.indoors.Value, p.Value)))
{
GameLocation oldIndoors = pair.Value;
@@ -86,10 +98,6 @@ namespace StardewModdingAPI.Framework.StateTracking
if (newIndoors != null)
this.Removed.Add(newIndoors);
}
-
- // update watchers
- foreach (IWatcher watcher in this.Locations)
- watcher.Update();
}
/// <summary>Set the current location list as the baseline.</summary>
@@ -98,21 +106,21 @@ namespace StardewModdingAPI.Framework.StateTracking
this.Removed.Clear();
this.Added.Clear();
this.LocationListWatcher.Reset();
+ this.MineLocationListWatcher.Reset();
}
/// <summary>Set the current value as the baseline.</summary>
public void Reset()
{
this.ResetLocationList();
- foreach (IWatcher watcher in this.Locations)
+ foreach (IWatcher watcher in this.GetWatchers())
watcher.Reset();
}
/// <summary>Stop watching the player fields and release all references.</summary>
public void Dispose()
{
- this.LocationListWatcher.Dispose();
- foreach (IWatcher watcher in this.Locations)
+ foreach (IWatcher watcher in this.GetWatchers())
watcher.Dispose();
}
@@ -180,11 +188,11 @@ namespace StardewModdingAPI.Framework.StateTracking
// remove old location if needed
this.Remove(location);
- // track change
+ // add location
this.Added.Add(location);
-
- // add
this.LocationDict[location] = new LocationTracker(location);
+
+ // add buildings
if (location is BuildableGameLocation buildableLocation)
this.Add(buildableLocation.buildings);
}
@@ -219,5 +227,17 @@ namespace StardewModdingAPI.Framework.StateTracking
this.Remove(buildableLocation.buildings);
}
}
+
+ /****
+ ** Helpers
+ ****/
+ /// <summary>The underlying watchers.</summary>
+ private IEnumerable<IWatcher> GetWatchers()
+ {
+ yield return this.LocationListWatcher;
+ yield return this.MineLocationListWatcher;
+ foreach (LocationTracker watcher in this.Locations)
+ yield return watcher;
+ }
}
}
diff --git a/src/SMAPI/Framework/WatcherCore.cs b/src/SMAPI/Framework/WatcherCore.cs
index e06423b9..8d29cf18 100644
--- a/src/SMAPI/Framework/WatcherCore.cs
+++ b/src/SMAPI/Framework/WatcherCore.cs
@@ -5,6 +5,7 @@ using StardewModdingAPI.Framework.Input;
using StardewModdingAPI.Framework.StateTracking;
using StardewModdingAPI.Framework.StateTracking.FieldWatchers;
using StardewValley;
+using StardewValley.Locations;
using StardewValley.Menus;
namespace StardewModdingAPI.Framework
@@ -64,7 +65,7 @@ namespace StardewModdingAPI.Framework
this.WindowSizeWatcher = WatcherFactory.ForEquatable(() => new Point(Game1.viewport.Width, Game1.viewport.Height));
this.TimeWatcher = WatcherFactory.ForEquatable(() => Game1.timeOfDay);
this.ActiveMenuWatcher = WatcherFactory.ForReference(() => Game1.activeClickableMenu);
- this.LocationsWatcher = new WorldLocationsTracker((ObservableCollection<GameLocation>)Game1.locations);
+ this.LocationsWatcher = new WorldLocationsTracker((ObservableCollection<GameLocation>)Game1.locations, MineShaft.activeMines);
this.LocaleWatcher = WatcherFactory.ForGenericEquality(() => LocalizedContentManager.CurrentLanguageCode);
this.Watchers.AddRange(new IWatcher[]
{