diff options
author | Jesse Plamondon-Willard <Pathoschild@users.noreply.github.com> | 2018-12-07 13:40:44 -0500 |
---|---|---|
committer | Jesse Plamondon-Willard <Pathoschild@users.noreply.github.com> | 2018-12-07 13:40:44 -0500 |
commit | a78b1935928919694dfe8de823a1accd6d222732 (patch) | |
tree | 3f17b6087cf2749e52c1e237de17e2e9addb6c06 /src/SMAPI/Framework | |
parent | 4cd9eda1591c3908bf80b60c2902491a7595ee27 (diff) | |
parent | 8901218418693d610a17b22fe789ba6279f63446 (diff) | |
download | SMAPI-a78b1935928919694dfe8de823a1accd6d222732.tar.gz SMAPI-a78b1935928919694dfe8de823a1accd6d222732.tar.bz2 SMAPI-a78b1935928919694dfe8de823a1accd6d222732.zip |
Merge branch 'develop' into stable
Diffstat (limited to 'src/SMAPI/Framework')
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[] { |