From b8a566a060eb5caa8cc37edba3ca670192f7a35b Mon Sep 17 00:00:00 2001 From: kchapelier Date: Mon, 6 Jan 2020 09:27:24 +0100 Subject: Add french translation --- docs/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'docs') diff --git a/docs/README.md b/docs/README.md index 3a570f48..50478b52 100644 --- a/docs/README.md +++ b/docs/README.md @@ -64,7 +64,7 @@ locale | status ---------- | :---------------- default | ✓ [fully translated](../src/SMAPI/i18n/default.json) Chinese | ✓ [fully translated](../src/SMAPI/i18n/zh.json) -French | ❑ not translated +French | ✓ [fully translated](../src/SMAPI/i18n/fr.json) German | ✓ [fully translated](../src/SMAPI/i18n/de.json) Hungarian | ❑ not translated Italian | ❑ not translated -- cgit From 18c69c5587f1196afc5c380cb078157e71b1a385 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 7 Jan 2020 21:26:58 -0500 Subject: intercept schedule errors --- docs/release-notes.md | 5 ++ src/SMAPI/Framework/SCore.cs | 3 +- src/SMAPI/Patches/ScheduleErrorPatch.cs | 86 +++++++++++++++++++++++++++++++++ 3 files changed, 93 insertions(+), 1 deletion(-) create mode 100644 src/SMAPI/Patches/ScheduleErrorPatch.cs (limited to 'docs') diff --git a/docs/release-notes.md b/docs/release-notes.md index ed6f9013..cf8fee7a 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -1,6 +1,11 @@ ← [README](README.md) # Release notes +## Upcoming release + +* For players: + * SMAPI now prevents mods from crashing the game with invalid schedule data. + ## 3.1 Released 05 January 2019 for Stardew Valley 1.4 or later. diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index dfd77e16..c4841ece 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -253,7 +253,8 @@ namespace StardewModdingAPI.Framework new DialogueErrorPatch(this.MonitorForGame, this.Reflection), new ObjectErrorPatch(), new LoadContextPatch(this.Reflection, this.GameInstance.OnLoadStageChanged), - new LoadErrorPatch(this.Monitor, this.GameInstance.OnSaveContentRemoved) + new LoadErrorPatch(this.Monitor, this.GameInstance.OnSaveContentRemoved), + new ScheduleErrorPatch(this.MonitorForGame) ); // add exit handler diff --git a/src/SMAPI/Patches/ScheduleErrorPatch.cs b/src/SMAPI/Patches/ScheduleErrorPatch.cs new file mode 100644 index 00000000..a23aa645 --- /dev/null +++ b/src/SMAPI/Patches/ScheduleErrorPatch.cs @@ -0,0 +1,86 @@ +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Reflection; +using Harmony; +using StardewModdingAPI.Framework.Patching; +using StardewValley; + +namespace StardewModdingAPI.Patches +{ + /// A Harmony patch for which intercepts crashes due to invalid schedule data. + /// Patch methods must be static for Harmony to work correctly. See the Harmony documentation before renaming patch arguments. + [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Argument names are defined by Harmony and methods are named for clarity.")] + [SuppressMessage("ReSharper", "IdentifierTypo", Justification = "Argument names are defined by Harmony and methods are named for clarity.")] + internal class ScheduleErrorPatch : IHarmonyPatch + { + /********* + ** Fields + *********/ + /// Writes messages to the console and log file on behalf of the game. + private static IMonitor MonitorForGame; + + /// Whether the target is currently being intercepted. + private static bool IsIntercepting; + + + /********* + ** Accessors + *********/ + /// A unique name for this patch. + public string Name => nameof(ScheduleErrorPatch); + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// Writes messages to the console and log file on behalf of the game. + public ScheduleErrorPatch(IMonitor monitorForGame) + { + ScheduleErrorPatch.MonitorForGame = monitorForGame; + } + + /// Apply the Harmony patch. + /// The Harmony instance. + public void Apply(HarmonyInstance harmony) + { + harmony.Patch( + original: AccessTools.Method(typeof(NPC), "parseMasterSchedule"), + prefix: new HarmonyMethod(this.GetType(), nameof(ScheduleErrorPatch.Before_NPC_parseMasterSchedule)) + ); + } + + + /********* + ** Private methods + *********/ + /// The method to call instead of . + /// The raw schedule data to parse. + /// The instance being patched. + /// The patched method's return value. + /// The method being wrapped. + /// Returns whether to execute the original method. + private static bool Before_NPC_parseMasterSchedule(string rawData, NPC __instance, ref Dictionary __result, MethodInfo __originalMethod) + { + if (ScheduleErrorPatch.IsIntercepting) + return true; + + try + { + ScheduleErrorPatch.IsIntercepting = true; + __result = (Dictionary)__originalMethod.Invoke(__instance, new object[] { rawData }); + return false; + } + catch (TargetInvocationException ex) + { + ScheduleErrorPatch.MonitorForGame.Log($"Failed parsing schedule for NPC {__instance.Name}:\n{rawData}\n{ex.InnerException ?? ex}", LogLevel.Error); + __result = new Dictionary(); + return false; + } + finally + { + ScheduleErrorPatch.IsIntercepting = false; + } + } + } +} -- cgit From ceff27c9a82bb16358aa0f390ce3f346c06c47bc Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 7 Jan 2020 21:29:49 -0500 Subject: update min game version 1.4.1 is needed due to the new gamepad option, which SMAPI 3.1 added support for. --- docs/release-notes.md | 1 + src/SMAPI/Constants.cs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) (limited to 'docs') diff --git a/docs/release-notes.md b/docs/release-notes.md index cf8fee7a..5aa279b5 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -5,6 +5,7 @@ * For players: * SMAPI now prevents mods from crashing the game with invalid schedule data. + * Updated minimum game version (1.4 → 1.4.1). ## 3.1 Released 05 January 2019 for Stardew Valley 1.4 or later. diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs index 97204d86..da2ee375 100644 --- a/src/SMAPI/Constants.cs +++ b/src/SMAPI/Constants.cs @@ -23,7 +23,7 @@ namespace StardewModdingAPI public static ISemanticVersion ApiVersion { get; } = new Toolkit.SemanticVersion("3.1.0"); /// The minimum supported version of Stardew Valley. - public static ISemanticVersion MinimumGameVersion { get; } = new GameVersion("1.4.0"); + public static ISemanticVersion MinimumGameVersion { get; } = new GameVersion("1.4.1"); /// The maximum supported version of Stardew Valley. public static ISemanticVersion MaximumGameVersion { get; } = null; -- cgit From 219696275df054d25cd385f950eb01ee33312e76 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 11 Jan 2020 13:20:37 -0500 Subject: fix errors due to async threads creating content managers --- docs/release-notes.md | 1 + src/SMAPI/Framework/ContentCoordinator.cs | 113 ++++++++++++++++++------------ src/SMAPI/Framework/InternalExtensions.cs | 70 ++++++++++++++++++ 3 files changed, 140 insertions(+), 44 deletions(-) (limited to 'docs') diff --git a/docs/release-notes.md b/docs/release-notes.md index 5aa279b5..dd4158aa 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -6,6 +6,7 @@ * For players: * SMAPI now prevents mods from crashing the game with invalid schedule data. * Updated minimum game version (1.4 → 1.4.1). + * Fixed 'collection was modified' error when returning to title in rare cases. ## 3.1 Released 05 January 2019 for Stardew Valley 1.4 or later. diff --git a/src/SMAPI/Framework/ContentCoordinator.cs b/src/SMAPI/Framework/ContentCoordinator.cs index 82d3805b..0e62837c 100644 --- a/src/SMAPI/Framework/ContentCoordinator.cs +++ b/src/SMAPI/Framework/ContentCoordinator.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; +using System.Threading; using Microsoft.Xna.Framework.Content; using StardewModdingAPI.Framework.Content; using StardewModdingAPI.Framework.ContentManagers; @@ -48,6 +49,10 @@ namespace StardewModdingAPI.Framework /// Whether the content coordinator has been disposed. private bool IsDisposed; + /// A lock used to prevent asynchronous changes to the content manager list. + /// The game may adds content managers in asynchronous threads (e.g. when populating the load screen). + private readonly ReaderWriterLockSlim ContentManagerLock = new ReaderWriterLockSlim(); + /********* ** Accessors @@ -96,9 +101,12 @@ namespace StardewModdingAPI.Framework /// A name for the mod manager. Not guaranteed to be unique. public GameContentManager CreateGameContentManager(string name) { - GameContentManager manager = new GameContentManager(name, this.MainContentManager.ServiceProvider, this.MainContentManager.RootDirectory, this.MainContentManager.CurrentCulture, this, this.Monitor, this.Reflection, this.OnDisposing, this.OnLoadingFirstAsset); - this.ContentManagers.Add(manager); - return manager; + return this.ContentManagerLock.InWriteLock(() => + { + GameContentManager manager = new GameContentManager(name, this.MainContentManager.ServiceProvider, this.MainContentManager.RootDirectory, this.MainContentManager.CurrentCulture, this, this.Monitor, this.Reflection, this.OnDisposing, this.OnLoadingFirstAsset); + this.ContentManagers.Add(manager); + return manager; + }); } /// Get a new content manager which handles reading files from a SMAPI mod folder with support for unpacked files. @@ -107,20 +115,23 @@ namespace StardewModdingAPI.Framework /// The game content manager used for map tilesheets not provided by the mod. public ModContentManager CreateModContentManager(string name, string rootDirectory, IContentManager gameContentManager) { - ModContentManager manager = new ModContentManager( - name: name, - gameContentManager: gameContentManager, - serviceProvider: this.MainContentManager.ServiceProvider, - rootDirectory: rootDirectory, - currentCulture: this.MainContentManager.CurrentCulture, - coordinator: this, - monitor: this.Monitor, - reflection: this.Reflection, - jsonHelper: this.JsonHelper, - onDisposing: this.OnDisposing - ); - this.ContentManagers.Add(manager); - return manager; + return this.ContentManagerLock.InWriteLock(() => + { + ModContentManager manager = new ModContentManager( + name: name, + gameContentManager: gameContentManager, + serviceProvider: this.MainContentManager.ServiceProvider, + rootDirectory: rootDirectory, + currentCulture: this.MainContentManager.CurrentCulture, + coordinator: this, + monitor: this.Monitor, + reflection: this.Reflection, + jsonHelper: this.JsonHelper, + onDisposing: this.OnDisposing + ); + this.ContentManagers.Add(manager); + return manager; + }); } /// Get the current content locale. @@ -132,8 +143,11 @@ namespace StardewModdingAPI.Framework /// Perform any cleanup needed when the locale changes. public void OnLocaleChanged() { - foreach (IContentManager contentManager in this.ContentManagers) - contentManager.OnLocaleChanged(); + this.ContentManagerLock.InReadLock(() => + { + foreach (IContentManager contentManager in this.ContentManagers) + contentManager.OnLocaleChanged(); + }); } /// Get whether this asset is mapped to a mod folder. @@ -179,13 +193,16 @@ namespace StardewModdingAPI.Framework /// The internal SMAPI asset key. public T LoadManagedAsset(string contentManagerID, string relativePath) { - // get content manager - IContentManager contentManager = this.ContentManagers.FirstOrDefault(p => p.IsNamespaced && p.Name == contentManagerID); - if (contentManager == null) - throw new InvalidOperationException($"The '{contentManagerID}' prefix isn't handled by any mod."); - - // get fresh asset - return contentManager.Load(relativePath, this.DefaultLanguage, useCache: false); + return this.ContentManagerLock.InReadLock(() => + { + // get content manager + IContentManager contentManager = this.ContentManagers.FirstOrDefault(p => p.IsNamespaced && p.Name == contentManagerID); + if (contentManager == null) + throw new InvalidOperationException($"The '{contentManagerID}' prefix isn't handled by any mod."); + + // get fresh asset + return contentManager.Load(relativePath, this.DefaultLanguage, useCache: false); + }); } /// Purge matched assets from the cache. @@ -208,28 +225,31 @@ namespace StardewModdingAPI.Framework /// Returns the invalidated asset names. public IEnumerable InvalidateCache(Func predicate, bool dispose = false) { - // invalidate cache & track removed assets - IDictionary> removedAssets = new Dictionary>(StringComparer.InvariantCultureIgnoreCase); - foreach (IContentManager contentManager in this.ContentManagers) + return this.ContentManagerLock.InReadLock(() => { - foreach (var entry in contentManager.InvalidateCache(predicate, dispose)) + // invalidate cache & track removed assets + IDictionary> removedAssets = new Dictionary>(StringComparer.InvariantCultureIgnoreCase); + foreach (IContentManager contentManager in this.ContentManagers) { - if (!removedAssets.TryGetValue(entry.Key, out ISet assets)) - removedAssets[entry.Key] = assets = new HashSet(new ObjectReferenceComparer()); - assets.Add(entry.Value); + foreach (var entry in contentManager.InvalidateCache(predicate, dispose)) + { + if (!removedAssets.TryGetValue(entry.Key, out ISet assets)) + removedAssets[entry.Key] = assets = new HashSet(new ObjectReferenceComparer()); + assets.Add(entry.Value); + } } - } - // reload core game assets - if (removedAssets.Any()) - { - IDictionary propagated = this.CoreAssets.Propagate(this.MainContentManager, removedAssets.ToDictionary(p => p.Key, p => p.Value.First().GetType())); // use an intercepted content manager - this.Monitor.Log($"Invalidated {removedAssets.Count} asset names ({string.Join(", ", removedAssets.Keys.OrderBy(p => p, StringComparer.InvariantCultureIgnoreCase))}); propagated {propagated.Count(p => p.Value)} core assets.", LogLevel.Trace); - } - else - this.Monitor.Log("Invalidated 0 cache entries.", LogLevel.Trace); + // reload core game assets + if (removedAssets.Any()) + { + IDictionary propagated = this.CoreAssets.Propagate(this.MainContentManager, removedAssets.ToDictionary(p => p.Key, p => p.Value.First().GetType())); // use an intercepted content manager + this.Monitor.Log($"Invalidated {removedAssets.Count} asset names ({string.Join(", ", removedAssets.Keys.OrderBy(p => p, StringComparer.InvariantCultureIgnoreCase))}); propagated {propagated.Count(p => p.Value)} core assets.", LogLevel.Trace); + } + else + this.Monitor.Log("Invalidated 0 cache entries.", LogLevel.Trace); - return removedAssets.Keys; + return removedAssets.Keys; + }); } /// Dispose held resources. @@ -244,6 +264,8 @@ namespace StardewModdingAPI.Framework contentManager.Dispose(); this.ContentManagers.Clear(); this.MainContentManager = null; + + this.ContentManagerLock.Dispose(); } @@ -257,7 +279,10 @@ namespace StardewModdingAPI.Framework if (this.IsDisposed) return; - this.ContentManagers.Remove(contentManager); + this.ContentManagerLock.InWriteLock(() => + { + this.ContentManagers.Remove(contentManager); + }); } } } diff --git a/src/SMAPI/Framework/InternalExtensions.cs b/src/SMAPI/Framework/InternalExtensions.cs index c3155b1c..8b45e196 100644 --- a/src/SMAPI/Framework/InternalExtensions.cs +++ b/src/SMAPI/Framework/InternalExtensions.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Reflection; +using System.Threading; using Microsoft.Xna.Framework.Graphics; using StardewModdingAPI.Framework.Events; using StardewModdingAPI.Framework.Reflection; @@ -83,6 +84,75 @@ namespace StardewModdingAPI.Framework return exception; } + /**** + ** ReaderWriterLockSlim + ****/ + /// Run code within a read lock. + /// The lock to set. + /// The action to perform. + public static void InReadLock(this ReaderWriterLockSlim @lock, Action action) + { + @lock.EnterReadLock(); + try + { + action(); + } + finally + { + @lock.ExitReadLock(); + } + } + + /// Run code within a read lock. + /// The action's return value. + /// The lock to set. + /// The action to perform. + public static TReturn InReadLock(this ReaderWriterLockSlim @lock, Func action) + { + @lock.EnterReadLock(); + try + { + return action(); + } + finally + { + @lock.ExitReadLock(); + } + } + + /// Run code within a write lock. + /// The lock to set. + /// The action to perform. + public static void InWriteLock(this ReaderWriterLockSlim @lock, Action action) + { + @lock.EnterWriteLock(); + try + { + action(); + } + finally + { + @lock.ExitWriteLock(); + } + } + + /// Run code within a write lock. + /// The action's return value. + /// The lock to set. + /// The action to perform. + public static TReturn InWriteLock(this ReaderWriterLockSlim @lock, Func action) + { + @lock.EnterWriteLock(); + try + { + return action(); + } + finally + { + @lock.ExitWriteLock(); + } + } + /**** ** Sprite batch ****/ -- cgit From bffc7f28e9abcdb2a3e5e8ee5353f4c2f7e111a0 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 11 Jan 2020 19:16:46 -0500 Subject: fix update-check error for Chucklefish pages with no version --- docs/release-notes.md | 1 + src/SMAPI.Web/Framework/Clients/Chucklefish/ChucklefishClient.cs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) (limited to 'docs') diff --git a/docs/release-notes.md b/docs/release-notes.md index dd4158aa..22e609dd 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -7,6 +7,7 @@ * SMAPI now prevents mods from crashing the game with invalid schedule data. * Updated minimum game version (1.4 → 1.4.1). * Fixed 'collection was modified' error when returning to title in rare cases. + * Fixed update-check error if a mod's Chucklefish page has no version. ## 3.1 Released 05 January 2019 for Stardew Valley 1.4 or later. diff --git a/src/SMAPI.Web/Framework/Clients/Chucklefish/ChucklefishClient.cs b/src/SMAPI.Web/Framework/Clients/Chucklefish/ChucklefishClient.cs index 939c32c6..cdb281e2 100644 --- a/src/SMAPI.Web/Framework/Clients/Chucklefish/ChucklefishClient.cs +++ b/src/SMAPI.Web/Framework/Clients/Chucklefish/ChucklefishClient.cs @@ -59,7 +59,7 @@ namespace StardewModdingAPI.Web.Framework.Clients.Chucklefish string name = doc.DocumentNode.SelectSingleNode("//meta[@name='twitter:title']").Attributes["content"].Value; if (name.StartsWith("[SMAPI] ")) name = name.Substring("[SMAPI] ".Length); - string version = doc.DocumentNode.SelectSingleNode("//h1/span").InnerText; + string version = doc.DocumentNode.SelectSingleNode("//h1/span")?.InnerText; // create model return new ChucklefishMod -- cgit From 2b68be4ebbb16213167b9bd04e891c7e05c67400 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 11 Jan 2020 20:33:01 -0500 Subject: add version mappings from the wiki to API data --- docs/release-notes.md | 3 +++ .../Framework/Clients/WebApi/ModExtendedMetadataModel.cs | 16 ++++++++++++++-- 2 files changed, 17 insertions(+), 2 deletions(-) (limited to 'docs') diff --git a/docs/release-notes.md b/docs/release-notes.md index 22e609dd..63cbbc2c 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -9,6 +9,9 @@ * Fixed 'collection was modified' error when returning to title in rare cases. * Fixed update-check error if a mod's Chucklefish page has no version. +* For SMAPI/tool developers: + * The `/mods` web API endpoint now includes version mappings from the wiki. + ## 3.1 Released 05 January 2019 for Stardew Valley 1.4 or later. diff --git a/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModExtendedMetadataModel.cs b/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModExtendedMetadataModel.cs index 4a697585..8c21e4e0 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModExtendedMetadataModel.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModExtendedMetadataModel.cs @@ -55,7 +55,7 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi /// The latest unofficial version, if newer than and . public ModEntryVersionModel Unofficial { get; set; } - /// The latest unofficial version for the current Stardew Valley or SMAPI beta, if any (see ). + /// The latest unofficial version for the current Stardew Valley or SMAPI beta, if any. public ModEntryVersionModel UnofficialForBeta { get; set; } /**** @@ -84,6 +84,15 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi /// The beta game or SMAPI version which broke this mod, if applicable. public string BetaBrokeIn { get; set; } + /**** + ** Version mappings + ****/ + /// Maps local versions to a semantic version for update checks. + public IDictionary MapLocalVersions { get; set; } + + /// Maps remote versions to a semantic version for update checks. + public IDictionary MapRemoteVersions { get; set; } + /********* ** Public methods @@ -127,13 +136,16 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi this.BetaCompatibilityStatus = wiki.BetaCompatibility?.Status; this.BetaCompatibilitySummary = wiki.BetaCompatibility?.Summary; this.BetaBrokeIn = wiki.BetaCompatibility?.BrokeIn; + + this.MapLocalVersions = wiki.MapLocalVersions; + this.MapRemoteVersions = wiki.MapRemoteVersions; } // internal DB data if (db != null) { this.ID = this.ID.Union(db.FormerIDs).ToArray(); - this.Name = this.Name ?? db.DisplayName; + this.Name ??= db.DisplayName; } } -- cgit From 5518e4cf241e487da26bd2e651a57724389edfe2 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 12 Jan 2020 15:45:54 -0500 Subject: fix asset propagation for player sprites not affecting other players or recolor maps --- docs/release-notes.md | 3 +++ src/SMAPI/Metadata/CoreAssetPropagator.cs | 33 +++++++++++++++++++++---------- 2 files changed, 26 insertions(+), 10 deletions(-) (limited to 'docs') diff --git a/docs/release-notes.md b/docs/release-notes.md index 63cbbc2c..f5c49a88 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -9,6 +9,9 @@ * Fixed 'collection was modified' error when returning to title in rare cases. * Fixed update-check error if a mod's Chucklefish page has no version. +For modders: + * Asset propagation for player sprites now affects other players' sprites, and updates recolor maps (e.g. sleeves). + * For SMAPI/tool developers: * The `/mods` web API endpoint now includes version mappings from the wiki. diff --git a/src/SMAPI/Metadata/CoreAssetPropagator.cs b/src/SMAPI/Metadata/CoreAssetPropagator.cs index b86a6790..abe28ce9 100644 --- a/src/SMAPI/Metadata/CoreAssetPropagator.cs +++ b/src/SMAPI/Metadata/CoreAssetPropagator.cs @@ -190,17 +190,9 @@ namespace StardewModdingAPI.Metadata case "characters\\farmer\\farmer_base": // Farmer case "characters\\farmer\\farmer_base_bald": - if (Game1.player == null || !Game1.player.IsMale) - return false; - Game1.player.FarmerRenderer = new FarmerRenderer(key, Game1.player); - return true; - - case "characters\\farmer\\farmer_girl_base": // Farmer + case "characters\\farmer\\farmer_girl_base": case "characters\\farmer\\farmer_girl_base_bald": - if (Game1.player == null || Game1.player.IsMale) - return false; - Game1.player.FarmerRenderer = new FarmerRenderer(key, Game1.player); - return true; + return this.ReloadPlayerSprites(key); case "characters\\farmer\\hairstyles": // Game1.LoadContent FarmerRenderer.hairStylesTexture = content.Load(key); @@ -835,6 +827,27 @@ namespace StardewModdingAPI.Metadata } } + /// Reload the sprites for matching players. + /// The asset key to reload. + private bool ReloadPlayerSprites(string key) + { + Farmer[] players = + ( + from player in Game1.getOnlineFarmers() + where key == this.NormalizeAssetNameIgnoringEmpty(player.getTexture()) + select player + ) + .ToArray(); + + foreach (Farmer player in players) + { + this.Reflection.GetField>>>(typeof(FarmerRenderer), "_recolorOffsets").GetValue().Remove(player.getTexture()); + player.FarmerRenderer.MarkSpriteDirty(); + } + + return players.Any(); + } + /// Reload tree textures. /// The content manager through which to reload the asset. /// The asset key to reload. -- cgit From 8b1fd90c6e72bff99d81a3b9614fdeaa6f67a950 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 12 Jan 2020 15:53:59 -0500 Subject: remove invalid-schedule error which can have false positives (e.g. when NPC is married to a player) --- docs/release-notes.md | 3 ++- src/SMAPI/Metadata/CoreAssetPropagator.cs | 16 +++++++--------- 2 files changed, 9 insertions(+), 10 deletions(-) (limited to 'docs') diff --git a/docs/release-notes.md b/docs/release-notes.md index f5c49a88..bc30bed7 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -11,8 +11,9 @@ For modders: * Asset propagation for player sprites now affects other players' sprites, and updates recolor maps (e.g. sleeves). + * Removed invalid-schedule validation which had false positives. -* For SMAPI/tool developers: +For SMAPI/tool developers: * The `/mods` web API endpoint now includes version mappings from the wiki. ## 3.1 diff --git a/src/SMAPI/Metadata/CoreAssetPropagator.cs b/src/SMAPI/Metadata/CoreAssetPropagator.cs index abe28ce9..57e1d197 100644 --- a/src/SMAPI/Metadata/CoreAssetPropagator.cs +++ b/src/SMAPI/Metadata/CoreAssetPropagator.cs @@ -909,18 +909,16 @@ namespace StardewModdingAPI.Metadata this.Reflection.GetField(villager, "_hasLoadedMasterScheduleData").SetValue(false); this.Reflection.GetField>(villager, "_masterScheduleData").SetValue(null); villager.Schedule = villager.getSchedule(Game1.dayOfMonth); - if (villager.Schedule == null) - { - this.Monitor.Log($"A mod set an invalid schedule for {villager.Name ?? key}, so the NPC may not behave correctly.", LogLevel.Warn); - return true; - } // switch to new schedule if needed - int lastScheduleTime = villager.Schedule.Keys.Where(p => p <= Game1.timeOfDay).OrderByDescending(p => p).FirstOrDefault(); - if (lastScheduleTime != 0) + if (villager.Schedule != null) { - villager.scheduleTimeToTry = NPC.NO_TRY; // use time that's passed in to checkSchedule - villager.checkSchedule(lastScheduleTime); + int lastScheduleTime = villager.Schedule.Keys.Where(p => p <= Game1.timeOfDay).OrderByDescending(p => p).FirstOrDefault(); + if (lastScheduleTime != 0) + { + villager.scheduleTimeToTry = NPC.NO_TRY; // use time that's passed in to checkSchedule + villager.checkSchedule(lastScheduleTime); + } } } return true; -- cgit From d68e4f97665898027f45782b8e66333912ae08ea Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 12 Jan 2020 19:41:14 -0500 Subject: drop pre-3.0 update-check support --- docs/release-notes.md | 1 + .../Framework/Clients/WebApi/ModEntryModel.cs | 22 ---------------------- src/SMAPI.Web/Controllers/ModsApiController.cs | 17 ++--------------- 3 files changed, 3 insertions(+), 37 deletions(-) (limited to 'docs') diff --git a/docs/release-notes.md b/docs/release-notes.md index bc30bed7..e2c6d500 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -15,6 +15,7 @@ For modders: For SMAPI/tool developers: * The `/mods` web API endpoint now includes version mappings from the wiki. + * Dropped API support for the pre-3.0 update-check format. ## 3.1 Released 05 January 2019 for Stardew Valley 1.4 or later. diff --git a/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModEntryModel.cs b/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModEntryModel.cs index f1bcfccc..2f58a3f1 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModEntryModel.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModEntryModel.cs @@ -1,5 +1,3 @@ -using System; - namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi { /// Metadata about a mod. @@ -17,26 +15,6 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi /// Optional extended data which isn't needed for update checks. public ModExtendedMetadataModel Metadata { get; set; } - /// The main version. - [Obsolete] - public ModEntryVersionModel Main { get; set; } - - /// The latest optional version, if newer than . - [Obsolete] - public ModEntryVersionModel Optional { get; set; } - - /// The latest unofficial version, if newer than and . - [Obsolete] - public ModEntryVersionModel Unofficial { get; set; } - - /// The latest unofficial version for the current Stardew Valley or SMAPI beta, if any (see ). - [Obsolete] - public ModEntryVersionModel UnofficialForBeta { get; set; } - - /// Whether a Stardew Valley or SMAPI beta which affects mod compatibility is in progress. If this is true, should be used for beta versions of SMAPI instead of . - [Obsolete] - public bool? HasBetaInfo { get; set; } - /// The errors that occurred while fetching update data. public string[] Errors { get; set; } = new string[0]; } diff --git a/src/SMAPI.Web/Controllers/ModsApiController.cs b/src/SMAPI.Web/Controllers/ModsApiController.cs index 3e3b81c8..f194b4d0 100644 --- a/src/SMAPI.Web/Controllers/ModsApiController.cs +++ b/src/SMAPI.Web/Controllers/ModsApiController.cs @@ -94,8 +94,6 @@ namespace StardewModdingAPI.Web.Controllers if (model?.Mods == null) return new ModEntryModel[0]; - bool legacyMode = SemanticVersion.TryParse(version, out ISemanticVersion parsedVersion) && parsedVersion.IsOlderThan("3.0.0-beta.20191109"); - // fetch wiki data WikiModEntry[] wikiData = this.WikiCache.GetWikiMods().Select(p => p.GetModel()).ToArray(); IDictionary mods = new Dictionary(StringComparer.CurrentCultureIgnoreCase); @@ -104,19 +102,8 @@ namespace StardewModdingAPI.Web.Controllers if (string.IsNullOrWhiteSpace(mod.ID)) continue; - ModEntryModel result = await this.GetModData(mod, wikiData, model.IncludeExtendedMetadata || legacyMode, model.ApiVersion); - if (legacyMode) - { - result.Main = result.Metadata.Main; - result.Optional = result.Metadata.Optional; - result.Unofficial = result.Metadata.Unofficial; - result.UnofficialForBeta = result.Metadata.UnofficialForBeta; - result.HasBetaInfo = result.Metadata.BetaCompatibilityStatus != null; - result.SuggestedUpdate = null; - if (!model.IncludeExtendedMetadata) - result.Metadata = null; - } - else if (!model.IncludeExtendedMetadata && (model.ApiVersion == null || mod.InstalledVersion == null)) + ModEntryModel result = await this.GetModData(mod, wikiData, model.IncludeExtendedMetadata, model.ApiVersion); + if (!model.IncludeExtendedMetadata && (model.ApiVersion == null || mod.InstalledVersion == null)) { var errors = new List(result.Errors); errors.Add($"This API can't suggest an update because {nameof(model.ApiVersion)} or {nameof(mod.InstalledVersion)} are null, and you didn't specify {nameof(model.IncludeExtendedMetadata)} to get other info. See the SMAPI technical docs for usage."); -- cgit From 700ea3cf1babe29d0365cdfec3cb2cd5a31e3f2b Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 12 Jan 2020 20:46:29 -0500 Subject: update release note (#691) --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) (limited to 'docs') diff --git a/docs/release-notes.md b/docs/release-notes.md index e2c6d500..fbf60573 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -8,6 +8,7 @@ * Updated minimum game version (1.4 → 1.4.1). * Fixed 'collection was modified' error when returning to title in rare cases. * Fixed update-check error if a mod's Chucklefish page has no version. + * Fixed SMAPI beta versions not showing update alert on next launch (thanks to danvolchek!). For modders: * Asset propagation for player sprites now affects other players' sprites, and updates recolor maps (e.g. sleeves). -- cgit From 25a22f5d7c527e60919b0e08a212578a323a8165 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 18 Jan 2020 16:21:16 -0500 Subject: update community links --- .github/CONTRIBUTING.md | 12 ++++-------- .github/SUPPORT.md | 3 +-- docs/release-notes.md | 1 + src/SMAPI.Web/Views/Index/Index.cshtml | 4 +++- src/SMAPI/Framework/SCore.cs | 2 +- 5 files changed, 10 insertions(+), 12 deletions(-) (limited to 'docs') diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 8746a487..74a7c500 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -1,16 +1,12 @@ Do you want to... -* **Ask for help using SMAPI?** - Please ask in [the Stardew Valley Discord or mod forums](https://smapi.io/community), don't - create a GitHub issue. - -* **Report a bug?** - Please report it in [the Stardew Valley Discord or mod forums](https://smapi.io/community), don't - create a GitHub issue unless you're sure it's a bug in the SMAPI code. +* **Ask for help or report a bug?** + Please see 'get help' on [the SMAPI website](https://smapi.io) instead, don't create a GitHub + issue. * **Submit a pull request?** Pull requests are welcome! If you're submitting a new feature, it's best to discuss first to make - sure it'll be accepted. Feel free to come chat [on Discord or in the SMAPI discussion thread](https://smapi.io/community). + sure it'll be accepted. Feel free to come chat [on Discord](https://smapi.io/community). Documenting your code and using the same formatting conventions is appreciated, but don't worry too much about it. We'll fix up the code after we accept the pull request if needed. diff --git a/.github/SUPPORT.md b/.github/SUPPORT.md index 9263666f..cb968c30 100644 --- a/.github/SUPPORT.md +++ b/.github/SUPPORT.md @@ -1,4 +1,3 @@ GitHub issues are only used for SMAPI development tasks. -To get help with SMAPI problems, [ask on Discord or in the forums](https://smapi.io/community) -instead. +To get help with SMAPI problems, see 'get help' on [the SMAPI website](https://smapi.io/) instead. diff --git a/docs/release-notes.md b/docs/release-notes.md index fbf60573..68ef3d71 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -15,6 +15,7 @@ For modders: * Removed invalid-schedule validation which had false positives. For SMAPI/tool developers: + * Updated links for the new r/SMAPI subreddit. * The `/mods` web API endpoint now includes version mappings from the wiki. * Dropped API support for the pre-3.0 update-check format. diff --git a/src/SMAPI.Web/Views/Index/Index.cshtml b/src/SMAPI.Web/Views/Index/Index.cshtml index 778da2d1..eded9df3 100644 --- a/src/SMAPI.Web/Views/Index/Index.cshtml +++ b/src/SMAPI.Web/Views/Index/Index.cshtml @@ -53,9 +53,11 @@

Get help

+ (Or join the community!)
diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index b80f8ddf..d71b5e5a 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -308,7 +308,7 @@ namespace StardewModdingAPI.Framework // show details if game crashed during last session if (File.Exists(Constants.FatalCrashMarker)) { - this.Monitor.Log("The game crashed last time you played. That can be due to bugs in the game, but if it happens repeatedly you can ask for help here: https://community.playstarbound.com/threads/108375/.", LogLevel.Error); + this.Monitor.Log("The game crashed last time you played. If it happens repeatedly, see 'get help' on https://smapi.io.", LogLevel.Error); this.Monitor.Log("If you ask for help, make sure to share your SMAPI log: https://smapi.io/log.", LogLevel.Error); this.Monitor.Log("Press any key to delete the crash data and continue playing.", LogLevel.Info); Console.ReadKey(); -- cgit From 1670a2f3a6263da158db5231f60d42d529734209 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 22 Jan 2020 19:06:33 -0500 Subject: fix global data stored in saves folder --- docs/release-notes.md | 1 + src/SMAPI.Installer/InteractiveInstaller.cs | 15 +++++++++++++++ src/SMAPI/Framework/ModHelpers/DataHelper.cs | 2 +- 3 files changed, 17 insertions(+), 1 deletion(-) (limited to 'docs') diff --git a/docs/release-notes.md b/docs/release-notes.md index 68ef3d71..b7bd7b53 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -13,6 +13,7 @@ For modders: * Asset propagation for player sprites now affects other players' sprites, and updates recolor maps (e.g. sleeves). * Removed invalid-schedule validation which had false positives. + * Fixed `helper.Data.Read/WriteGlobalData` using the `Saves` folder. The installer will move existing folders to the appdata folder. For SMAPI/tool developers: * Updated links for the new r/SMAPI subreddit. diff --git a/src/SMAPI.Installer/InteractiveInstaller.cs b/src/SMAPI.Installer/InteractiveInstaller.cs index 964300ac..14f37258 100644 --- a/src/SMAPI.Installer/InteractiveInstaller.cs +++ b/src/SMAPI.Installer/InteractiveInstaller.cs @@ -373,6 +373,21 @@ namespace StardewModdingApi.Installer this.InteractivelyDelete(path); } + // move global save data folder (changed in 3.2) + { + string dataPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "StardewValley"); + DirectoryInfo oldDir = new DirectoryInfo(Path.Combine(dataPath, "Saves", ".smapi")); + DirectoryInfo newDir = new DirectoryInfo(Path.Combine(dataPath, ".smapi")); + + if (oldDir.Exists) + { + if (newDir.Exists) + this.InteractivelyDelete(oldDir.FullName); + else + oldDir.MoveTo(newDir.FullName); + } + } + /**** ** Install new files ****/ diff --git a/src/SMAPI/Framework/ModHelpers/DataHelper.cs b/src/SMAPI/Framework/ModHelpers/DataHelper.cs index 3d43c539..6cde849c 100644 --- a/src/SMAPI/Framework/ModHelpers/DataHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/DataHelper.cs @@ -177,7 +177,7 @@ namespace StardewModdingAPI.Framework.ModHelpers private string GetGlobalDataPath(string key) { this.AssertSlug(key, nameof(key)); - return Path.Combine(Constants.SavesPath, ".smapi", "mod-data", this.ModID.ToLower(), $"{key}.json".ToLower()); + return Path.Combine(Constants.DataPath, ".smapi", "mod-data", this.ModID.ToLower(), $"{key}.json".ToLower()); } /// Assert that a key contains only characters that are safe in all contexts. -- cgit From 381de5eba9f9822c3483abdf64396cec794e3d03 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 22 Jan 2020 20:36:24 -0500 Subject: add test_input console command --- docs/release-notes.md | 14 +++-- .../Framework/Commands/ITrainerCommand.cs | 14 +++-- .../Framework/Commands/Other/TestInputCommand.cs | 59 ++++++++++++++++++++++ .../Framework/Commands/Player/SetHealthCommand.cs | 15 ++---- .../Framework/Commands/Player/SetMoneyCommand.cs | 13 ++--- .../Framework/Commands/Player/SetStaminaCommand.cs | 15 ++---- .../Framework/Commands/TrainerCommand.cs | 22 ++++++-- .../Framework/Commands/World/FreezeTimeCommand.cs | 15 ++---- src/SMAPI.Mods.ConsoleCommands/ModEntry.cs | 31 +++++++++--- 9 files changed, 134 insertions(+), 64 deletions(-) create mode 100644 src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/TestInputCommand.cs (limited to 'docs') diff --git a/docs/release-notes.md b/docs/release-notes.md index b7bd7b53..8dac1d0c 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -10,6 +10,9 @@ * Fixed update-check error if a mod's Chucklefish page has no version. * Fixed SMAPI beta versions not showing update alert on next launch (thanks to danvolchek!). +For the Console Commands mod: + * Added `test_input` command to view button codes in the console. + For modders: * Asset propagation for player sprites now affects other players' sprites, and updates recolor maps (e.g. sleeves). * Removed invalid-schedule validation which had false positives. @@ -31,13 +34,14 @@ Released 05 January 2019 for Stardew Valley 1.4 or later. * Fixed compatibility with Linux Mint 18 (thanks to techge!), Arch Linux, and Linux systems with libhybris-utils installed. * Fixed memory leak when repeatedly loading a save and returning to title. * Fixed memory leak when mods reload assets. - * Fixes for Console Commands mod: - * added new clothing items; - * fixed spawning new flooring and rings (thanks to Mizzion!); - * fixed spawning custom rings added by mods; - * Fixed errors when some item data is invalid. * Updated translations. Thanks to L30Bola (added Portuguese), PlussRolf (added Spanish), and shirutan (added Japanese)! +* For the Console Commands mod: + * Added new clothing items. + * Fixed spawning new flooring and rings (thanks to Mizzion!). + * Fixed spawning custom rings added by mods. + * Fixed errors when some item data is invalid. + * For the web UI: * Added option to edit & reupload in the JSON validator. * File uploads are now stored in Azure storage instead of Pastebin, due to ongoing Pastebin perfomance issues. diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/ITrainerCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/ITrainerCommand.cs index a0b739f8..d4d36e5d 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/ITrainerCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/ITrainerCommand.cs @@ -12,8 +12,11 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands /// The command description. string Description { get; } - /// Whether the command needs to perform logic when the game updates. - bool NeedsUpdate { get; } + /// Whether the command may need to perform logic when the game updates. This value shouldn't change. + bool MayNeedUpdate { get; } + + /// Whether the command may need to perform logic when the player presses a button. This value shouldn't change. + bool MayNeedInput { get; } /********* @@ -27,6 +30,11 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands /// Perform any logic needed on update tick. /// Writes messages to the console and log file. - void Update(IMonitor monitor); + void OnUpdated(IMonitor monitor); + + /// Perform any logic when input is received. + /// Writes messages to the console and log file. + /// The button that was pressed. + void OnButtonPressed(IMonitor monitor, SButton button); } } diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/TestInputCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/TestInputCommand.cs new file mode 100644 index 00000000..11aa10c3 --- /dev/null +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/TestInputCommand.cs @@ -0,0 +1,59 @@ +using System; + +namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other +{ + /// A command which logs the keys being pressed for 30 seconds once enabled. + internal class TestInputCommand : TrainerCommand + { + /********* + ** Fields + *********/ + /// The number of seconds for which to log input. + private readonly int LogSeconds = 30; + + /// When the command should stop printing input, or null if currently disabled. + private long? ExpiryTicks; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + public TestInputCommand() + : base("test_input", "Prints all input to the console for 30 seconds.", mayNeedUpdate: true, mayNeedInput: true) { } + + /// Handle the command. + /// Writes messages to the console and log file. + /// The command name. + /// The command arguments. + public override void Handle(IMonitor monitor, string command, ArgumentParser args) + { + this.ExpiryTicks = DateTime.UtcNow.Add(TimeSpan.FromSeconds(this.LogSeconds)).Ticks; + monitor.Log($"OK, logging all player input for {this.LogSeconds} seconds.", LogLevel.Info); + } + + /// Perform any logic needed on update tick. + /// Writes messages to the console and log file. + public override void OnUpdated(IMonitor monitor) + { + // handle expiry + if (this.ExpiryTicks == null) + return; + if (this.ExpiryTicks <= DateTime.UtcNow.Ticks) + { + monitor.Log("No longer logging input.", LogLevel.Info); + this.ExpiryTicks = null; + return; + } + } + + /// Perform any logic when input is received. + /// Writes messages to the console and log file. + /// The button that was pressed. + public override void OnButtonPressed(IMonitor monitor, SButton button) + { + if (this.ExpiryTicks != null) + monitor.Log($"Pressed {button}", LogLevel.Info); + } + } +} diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetHealthCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetHealthCommand.cs index 1abb82b5..59bda5dd 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetHealthCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetHealthCommand.cs @@ -1,4 +1,4 @@ -using System.Linq; +using System.Linq; using StardewValley; namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player @@ -13,19 +13,12 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player private bool InfiniteHealth; - /********* - ** Accessors - *********/ - /// Whether the command needs to perform logic when the game updates. - public override bool NeedsUpdate => this.InfiniteHealth; - - /********* ** Public methods *********/ /// Construct an instance. public SetHealthCommand() - : base("player_sethealth", "Sets the player's health.\n\nUsage: player_sethealth [value]\n- value: an integer amount, or 'inf' for infinite health.") { } + : base("player_sethealth", "Sets the player's health.\n\nUsage: player_sethealth [value]\n- value: an integer amount, or 'inf' for infinite health.", mayNeedUpdate: true) { } /// Handle the command. /// Writes messages to the console and log file. @@ -62,9 +55,9 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player /// Perform any logic needed on update tick. /// Writes messages to the console and log file. - public override void Update(IMonitor monitor) + public override void OnUpdated(IMonitor monitor) { - if (this.InfiniteHealth) + if (this.InfiniteHealth && Context.IsWorldReady) Game1.player.health = Game1.player.maxHealth; } } diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetMoneyCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetMoneyCommand.cs index 1706bbc1..6e3d68b6 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetMoneyCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetMoneyCommand.cs @@ -13,19 +13,12 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player private bool InfiniteMoney; - /********* - ** Accessors - *********/ - /// Whether the command needs to perform logic when the game updates. - public override bool NeedsUpdate => this.InfiniteMoney; - - /********* ** Public methods *********/ /// Construct an instance. public SetMoneyCommand() - : base("player_setmoney", "Sets the player's money.\n\nUsage: player_setmoney \n- value: an integer amount, or 'inf' for infinite money.") { } + : base("player_setmoney", "Sets the player's money.\n\nUsage: player_setmoney \n- value: an integer amount, or 'inf' for infinite money.", mayNeedUpdate: true) { } /// Handle the command. /// Writes messages to the console and log file. @@ -62,9 +55,9 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player /// Perform any logic needed on update tick. /// Writes messages to the console and log file. - public override void Update(IMonitor monitor) + public override void OnUpdated(IMonitor monitor) { - if (this.InfiniteMoney) + if (this.InfiniteMoney && Context.IsWorldReady) Game1.player.Money = 999999; } } diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetStaminaCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetStaminaCommand.cs index 009cb1de..60a1dcb1 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetStaminaCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetStaminaCommand.cs @@ -1,4 +1,4 @@ -using System.Linq; +using System.Linq; using StardewValley; namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player @@ -13,19 +13,12 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player private bool InfiniteStamina; - /********* - ** Accessors - *********/ - /// Whether the command needs to perform logic when the game updates. - public override bool NeedsUpdate => this.InfiniteStamina; - - /********* ** Public methods *********/ /// Construct an instance. public SetStaminaCommand() - : base("player_setstamina", "Sets the player's stamina.\n\nUsage: player_setstamina [value]\n- value: an integer amount, or 'inf' for infinite stamina.") { } + : base("player_setstamina", "Sets the player's stamina.\n\nUsage: player_setstamina [value]\n- value: an integer amount, or 'inf' for infinite stamina.", mayNeedUpdate: true) { } /// Handle the command. /// Writes messages to the console and log file. @@ -62,9 +55,9 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player /// Perform any logic needed on update tick. /// Writes messages to the console and log file. - public override void Update(IMonitor monitor) + public override void OnUpdated(IMonitor monitor) { - if (this.InfiniteStamina) + if (this.InfiniteStamina && Context.IsWorldReady) Game1.player.stamina = Game1.player.MaxStamina; } } diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/TrainerCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/TrainerCommand.cs index 466b8f6e..6d5cae97 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/TrainerCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/TrainerCommand.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; @@ -16,8 +16,11 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands /// The command description. public string Description { get; } - /// Whether the command needs to perform logic when the game updates. - public virtual bool NeedsUpdate { get; } = false; + /// Whether the command may need to perform logic when the player presses a button. This value shouldn't change. + public bool MayNeedInput { get; } + + /// Whether the command may need to perform logic when the game updates. This value shouldn't change. + public bool MayNeedUpdate { get; } /********* @@ -31,7 +34,12 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands /// Perform any logic needed on update tick. /// Writes messages to the console and log file. - public virtual void Update(IMonitor monitor) { } + public virtual void OnUpdated(IMonitor monitor) { } + + /// Perform any logic when input is received. + /// Writes messages to the console and log file. + /// The button that was pressed. + public virtual void OnButtonPressed(IMonitor monitor, SButton button) { } /********* @@ -40,10 +48,14 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands /// Construct an instance. /// The command name the user must type. /// The command description. - protected TrainerCommand(string name, string description) + /// Whether the command may need to perform logic when the player presses a button. + /// Whether the command may need to perform logic when the game updates. + protected TrainerCommand(string name, string description, bool mayNeedInput = false, bool mayNeedUpdate = false) { this.Name = name; this.Description = description; + this.MayNeedInput = mayNeedInput; + this.MayNeedUpdate = mayNeedUpdate; } /// Log an error indicating incorrect usage. diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/FreezeTimeCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/FreezeTimeCommand.cs index 6a7ab162..736a93a0 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/FreezeTimeCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/FreezeTimeCommand.cs @@ -1,4 +1,4 @@ -using System.Linq; +using System.Linq; using StardewValley; namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World @@ -16,19 +16,12 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World private bool FreezeTime; - /********* - ** Accessors - *********/ - /// Whether the command needs to perform logic when the game updates. - public override bool NeedsUpdate => this.FreezeTime; - - /********* ** Public methods *********/ /// Construct an instance. public FreezeTimeCommand() - : base("world_freezetime", "Freezes or resumes time.\n\nUsage: world_freezetime [value]\n- value: one of 0 (resume), 1 (freeze), or blank (toggle).") { } + : base("world_freezetime", "Freezes or resumes time.\n\nUsage: world_freezetime [value]\n- value: one of 0 (resume), 1 (freeze), or blank (toggle).", mayNeedUpdate: true) { } /// Handle the command. /// Writes messages to the console and log file. @@ -57,9 +50,9 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World /// Perform any logic needed on update tick. /// Writes messages to the console and log file. - public override void Update(IMonitor monitor) + public override void OnUpdated(IMonitor monitor) { - if (this.FreezeTime) + if (this.FreezeTime && Context.IsWorldReady) Game1.timeOfDay = FreezeTimeCommand.FrozenTime; } } diff --git a/src/SMAPI.Mods.ConsoleCommands/ModEntry.cs b/src/SMAPI.Mods.ConsoleCommands/ModEntry.cs index 4807c46d..5c4f3bba 100644 --- a/src/SMAPI.Mods.ConsoleCommands/ModEntry.cs +++ b/src/SMAPI.Mods.ConsoleCommands/ModEntry.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using StardewModdingAPI.Events; using StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands; namespace StardewModdingAPI.Mods.ConsoleCommands @@ -14,6 +15,12 @@ namespace StardewModdingAPI.Mods.ConsoleCommands /// The commands to handle. private ITrainerCommand[] Commands; + /// The commands which may need to handle update ticks. + private ITrainerCommand[] UpdateHandlers; + + /// The commands which may need to handle input. + private ITrainerCommand[] InputHandlers; + /********* ** Public methods @@ -27,27 +34,35 @@ namespace StardewModdingAPI.Mods.ConsoleCommands foreach (ITrainerCommand command in this.Commands) helper.ConsoleCommands.Add(command.Name, command.Description, (name, args) => this.HandleCommand(command, name, args)); + // cache commands + this.InputHandlers = this.Commands.Where(p => p.MayNeedInput).ToArray(); + this.UpdateHandlers = this.Commands.Where(p => p.MayNeedUpdate).ToArray(); + // hook events helper.Events.GameLoop.UpdateTicked += this.OnUpdateTicked; + helper.Events.Input.ButtonPressed += this.OnButtonPressed; } /********* ** Private methods *********/ + /// The method invoked when a button is pressed. + /// The event sender. + /// The event arguments. + private void OnButtonPressed(object sender, ButtonPressedEventArgs e) + { + foreach (ITrainerCommand command in this.InputHandlers) + command.OnButtonPressed(this.Monitor, e.Button); + } + /// The method invoked when the game updates its state. /// The event sender. /// The event arguments. private void OnUpdateTicked(object sender, EventArgs e) { - if (!Context.IsWorldReady) - return; - - foreach (ITrainerCommand command in this.Commands) - { - if (command.NeedsUpdate) - command.Update(this.Monitor); - } + foreach (ITrainerCommand command in this.UpdateHandlers) + command.OnUpdated(this.Monitor); } /// Handle a console command. -- cgit From d1935e686c6396519a1ff9b1b429cd55adcf8d11 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 23 Jan 2020 00:31:26 -0500 Subject: add full internal support for non-standard four-part versions --- docs/release-notes.md | 7 +- src/SMAPI.Tests/Utilities/SemanticVersionTests.cs | 208 ++++++++++++++++----- .../ISemanticVersion.cs | 3 + .../Framework/SemanticVersionReader.cs | 126 +++++++++++++ src/SMAPI.Toolkit/SemanticVersion.cs | 72 ++++--- .../Converters/SemanticVersionConverter.cs | 2 +- src/SMAPI.Web/Framework/LogParsing/LogParser.cs | 9 +- src/SMAPI.Web/Framework/VersionConstraint.cs | 28 ++- src/SMAPI/Framework/GameVersion.cs | 30 +-- src/SMAPI/SemanticVersion.cs | 34 +++- 10 files changed, 407 insertions(+), 112 deletions(-) create mode 100644 src/SMAPI.Toolkit/Framework/SemanticVersionReader.cs (limited to 'docs') diff --git a/docs/release-notes.md b/docs/release-notes.md index 8dac1d0c..35fdbb4b 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -10,15 +10,16 @@ * Fixed update-check error if a mod's Chucklefish page has no version. * Fixed SMAPI beta versions not showing update alert on next launch (thanks to danvolchek!). -For the Console Commands mod: +* For the Console Commands mod: * Added `test_input` command to view button codes in the console. -For modders: +* For modders: * Asset propagation for player sprites now affects other players' sprites, and updates recolor maps (e.g. sleeves). * Removed invalid-schedule validation which had false positives. * Fixed `helper.Data.Read/WriteGlobalData` using the `Saves` folder. The installer will move existing folders to the appdata folder. -For SMAPI/tool developers: +* For SMAPI/tool developers: + * Added internal support for four-part versions to support SMAPI on Android. * Updated links for the new r/SMAPI subreddit. * The `/mods` web API endpoint now includes version mappings from the wiki. * Dropped API support for the pre-3.0 update-check format. diff --git a/src/SMAPI.Tests/Utilities/SemanticVersionTests.cs b/src/SMAPI.Tests/Utilities/SemanticVersionTests.cs index 48afcaa2..ac4ef39b 100644 --- a/src/SMAPI.Tests/Utilities/SemanticVersionTests.cs +++ b/src/SMAPI.Tests/Utilities/SemanticVersionTests.cs @@ -17,7 +17,8 @@ namespace SMAPI.Tests.Utilities /**** ** Constructor ****/ - [Test(Description = "Assert that the constructor sets the expected values for all valid versions when constructed from a string.")] + /// Assert the parsed version when constructed from a standard string. + /// The version string to parse. [TestCase("1.0", ExpectedResult = "1.0.0")] [TestCase("1.0.0", ExpectedResult = "1.0.0")] [TestCase("3000.4000.5000", ExpectedResult = "3000.4000.5000")] @@ -29,10 +30,76 @@ namespace SMAPI.Tests.Utilities [TestCase("1.2+3.4.5-build.004", ExpectedResult = "1.2.0+3.4.5-build.004")] public string Constructor_FromString(string input) { - return new SemanticVersion(input).ToString(); + // act + ISemanticVersion version = new SemanticVersion(input); + + // assert + return version.ToString(); } - [Test(Description = "Assert that the constructor sets the expected values for all valid versions when constructed from the individual numbers.")] + + /// Assert that the constructor rejects invalid values when constructed from a string. + /// The version string to parse. + [Test(Description = "Assert that the constructor throws the expected exception for invalid versions.")] + [TestCase(null)] + [TestCase("")] + [TestCase(" ")] + [TestCase("1")] + [TestCase("01.0")] + [TestCase("1.05")] + [TestCase("1.5.06")] // leading zeros specifically prohibited by spec + [TestCase("1.2.3.4")] + [TestCase("1.apple")] + [TestCase("1.2.apple")] + [TestCase("1.2.3.apple")] + [TestCase("1..2..3")] + [TestCase("1.2.3-")] + [TestCase("1.2.3--some-tag")] + [TestCase("1.2.3-some-tag...")] + [TestCase("1.2.3-some-tag...4")] + [TestCase("1.2.3-some-tag.4+build...4")] + [TestCase("apple")] + [TestCase("-apple")] + [TestCase("-5")] + public void Constructor_FromString_WithInvalidValues(string input) + { + if (input == null) + this.AssertAndLogException(() => new SemanticVersion(input)); + else + this.AssertAndLogException(() => new SemanticVersion(input)); + } + + /// Assert the parsed version when constructed from a non-standard string. + /// The version string to parse. + [TestCase("1.2.3", ExpectedResult = "1.2.3")] + [TestCase("1.0.0.0", ExpectedResult = "1.0.0")] + [TestCase("1.0.0.5", ExpectedResult = "1.0.0.5")] + [TestCase("1.2.3.4-some-tag.4 ", ExpectedResult = "1.2.3.4-some-tag.4")] + public string Constructor_FromString_NonStandard(string input) + { + // act + ISemanticVersion version = new SemanticVersion(input, allowNonStandard: true); + + // assert + return version.ToString(); + } + + /// Assert that the constructor rejects a non-standard string when the non-standard flag isn't set. + /// The version string to parse. + [TestCase("1.0.0.0")] + [TestCase("1.0.0.5")] + [TestCase("1.2.3.4-some-tag.4 ")] + public void Constructor_FromString_Standard_DisallowsNonStandardVersion(string input) + { + Assert.Throws(() => new SemanticVersion(input)); + } + + /// Assert the parsed version when constructed from standard parts. + /// The major number. + /// The minor number. + /// The patch number. + /// The prerelease tag. + /// The build metadata. [TestCase(1, 0, 0, null, null, ExpectedResult = "1.0.0")] [TestCase(3000, 4000, 5000, null, null, ExpectedResult = "3000.4000.5000")] [TestCase(1, 2, 3, "", null, ExpectedResult = "1.2.3")] @@ -49,15 +116,43 @@ namespace SMAPI.Tests.Utilities ISemanticVersion version = new SemanticVersion(major, minor, patch, prerelease, build); // assert - Assert.AreEqual(major, version.MajorVersion, "The major version doesn't match the given value."); - Assert.AreEqual(minor, version.MinorVersion, "The minor version doesn't match the given value."); - Assert.AreEqual(patch, version.PatchVersion, "The patch version doesn't match the given value."); - Assert.AreEqual(string.IsNullOrWhiteSpace(prerelease) ? null : prerelease.Trim(), version.PrereleaseTag, "The prerelease tag doesn't match the given value."); - Assert.AreEqual(string.IsNullOrWhiteSpace(build) ? null : build.Trim(), version.BuildMetadata, "The build metadata doesn't match the given value."); + this.AssertParts(version, major, minor, patch, prerelease, build, nonStandard: false); return version.ToString(); } - [Test(Description = "Assert that the constructor throws the expected exception for invalid versions when constructed from the individual numbers.")] + /// Assert the parsed version when constructed from parts including non-standard fields. + /// The major number. + /// The minor number. + /// The patch number. + /// The non-standard platform release number. + /// The prerelease tag. + /// The build metadata. + [TestCase(1, 0, 0, 0, null, null, ExpectedResult = "1.0.0")] + [TestCase(3000, 4000, 5000, 6000, null, null, ExpectedResult = "3000.4000.5000.6000")] + [TestCase(1, 2, 3, 4, "", null, ExpectedResult = "1.2.3.4")] + [TestCase(1, 2, 3, 4, " ", null, ExpectedResult = "1.2.3.4")] + [TestCase(1, 2, 3, 4, "0", null, ExpectedResult = "1.2.3.4-0")] + [TestCase(1, 2, 3, 4, "some-tag.4", null, ExpectedResult = "1.2.3.4-some-tag.4")] + [TestCase(1, 2, 3, 4, "sOMe-TaG.4", null, ExpectedResult = "1.2.3.4-sOMe-TaG.4")] + [TestCase(1, 2, 3, 4, "some-tag.4 ", null, ExpectedResult = "1.2.3.4-some-tag.4")] + [TestCase(1, 2, 3, 4, "some-tag.4 ", "build.004", ExpectedResult = "1.2.3.4-some-tag.4+build.004")] + [TestCase(1, 2, 0, 4, null, "3.4.5-build.004", ExpectedResult = "1.2.0.4+3.4.5-build.004")] + public string Constructor_FromParts_NonStandard(int major, int minor, int patch, int platformRelease, string prerelease, string build) + { + // act + ISemanticVersion version = new SemanticVersion(major, minor, patch, platformRelease, prerelease, build); + + // assert + this.AssertParts(version, major, minor, patch, prerelease, build, nonStandard: platformRelease != 0); + return version.ToString(); + } + + /// Assert that the constructor rejects invalid values when constructed from the individual numbers. + /// The major number. + /// The minor number. + /// The patch number. + /// The prerelease tag. + /// The build metadata. [TestCase(0, 0, 0, null, null)] [TestCase(-1, 0, 0, null, null)] [TestCase(0, -1, 0, null, null)] @@ -71,6 +166,10 @@ namespace SMAPI.Tests.Utilities this.AssertAndLogException(() => new SemanticVersion(major, minor, patch, prerelease, build)); } + /// Assert the parsed version when constructed from an assembly version. + /// The major number. + /// The minor number. + /// The patch number. [Test(Description = "Assert that the constructor sets the expected values for all valid versions when constructed from an assembly version.")] [TestCase(1, 0, 0, ExpectedResult = "1.0.0")] [TestCase(1, 2, 3, ExpectedResult = "1.2.3")] @@ -81,45 +180,16 @@ namespace SMAPI.Tests.Utilities ISemanticVersion version = new SemanticVersion(new Version(major, minor, patch)); // assert - Assert.AreEqual(major, version.MajorVersion, "The major version doesn't match the given value."); - Assert.AreEqual(minor, version.MinorVersion, "The minor version doesn't match the given value."); - Assert.AreEqual(patch, version.PatchVersion, "The patch version doesn't match the given value."); + this.AssertParts(version, major, minor, patch, null, null, nonStandard: false); return version.ToString(); } - [Test(Description = "Assert that the constructor throws the expected exception for invalid versions.")] - [TestCase(null)] - [TestCase("")] - [TestCase(" ")] - [TestCase("1")] - [TestCase("01.0")] - [TestCase("1.05")] - [TestCase("1.5.06")] // leading zeros specifically prohibited by spec - [TestCase("1.2.3.4")] - [TestCase("1.apple")] - [TestCase("1.2.apple")] - [TestCase("1.2.3.apple")] - [TestCase("1..2..3")] - [TestCase("1.2.3-")] - [TestCase("1.2.3--some-tag")] - [TestCase("1.2.3-some-tag...")] - [TestCase("1.2.3-some-tag...4")] - [TestCase("1.2.3-some-tag.4+build...4")] - [TestCase("apple")] - [TestCase("-apple")] - [TestCase("-5")] - public void Constructor_FromString_WithInvalidValues(string input) - { - if (input == null) - this.AssertAndLogException(() => new SemanticVersion(input)); - else - this.AssertAndLogException(() => new SemanticVersion(input)); - } - /**** ** CompareTo ****/ - [Test(Description = "Assert that version.CompareTo returns the expected value.")] + /// Assert that returns the expected value. + /// The left version. + /// The right version. // equal [TestCase("0.5.7", "0.5.7", ExpectedResult = 0)] [TestCase("1.0", "1.0", ExpectedResult = 0)] @@ -149,15 +219,20 @@ namespace SMAPI.Tests.Utilities [TestCase("1.0-beta-10", "1.0-beta-2", ExpectedResult = 1)] public int CompareTo(string versionStrA, string versionStrB) { + // arrange ISemanticVersion versionA = new SemanticVersion(versionStrA); ISemanticVersion versionB = new SemanticVersion(versionStrB); + + // assert return versionA.CompareTo(versionB); } /**** ** IsOlderThan ****/ - [Test(Description = "Assert that version.IsOlderThan returns the expected value.")] + /// Assert that and return the expected value. + /// The left version. + /// The right version. // keep test cases in sync with CompareTo for simplicity. // equal [TestCase("0.5.7", "0.5.7", ExpectedResult = false)] @@ -187,15 +262,21 @@ namespace SMAPI.Tests.Utilities [TestCase("1.0-beta-10", "1.0-beta-2", ExpectedResult = false)] public bool IsOlderThan(string versionStrA, string versionStrB) { + // arrange ISemanticVersion versionA = new SemanticVersion(versionStrA); ISemanticVersion versionB = new SemanticVersion(versionStrB); + + // assert + Assert.AreEqual(versionA.IsOlderThan(versionB), versionA.IsOlderThan(versionB.ToString()), "The two signatures returned different results."); return versionA.IsOlderThan(versionB); } /**** ** IsNewerThan ****/ - [Test(Description = "Assert that version.IsNewerThan returns the expected value.")] + /// Assert that and return the expected value. + /// The left version. + /// The right version. // keep test cases in sync with CompareTo for simplicity. // equal [TestCase("0.5.7", "0.5.7", ExpectedResult = false)] @@ -225,14 +306,22 @@ namespace SMAPI.Tests.Utilities [TestCase("1.0-beta-10", "1.0-beta-2", ExpectedResult = true)] public bool IsNewerThan(string versionStrA, string versionStrB) { + // arrange ISemanticVersion versionA = new SemanticVersion(versionStrA); ISemanticVersion versionB = new SemanticVersion(versionStrB); + + // assert + Assert.AreEqual(versionA.IsNewerThan(versionB), versionA.IsNewerThan(versionB.ToString()), "The two signatures returned different results."); return versionA.IsNewerThan(versionB); } /**** ** IsBetween ****/ + /// Assert that and return the expected value. + /// The main version. + /// The lower version number. + /// The upper version number. [Test(Description = "Assert that version.IsNewerThan returns the expected value.")] // is between [TestCase("0.5.7-beta.3", "0.5.7-beta.3", "0.5.7-beta.3", ExpectedResult = true)] @@ -250,17 +339,24 @@ namespace SMAPI.Tests.Utilities [TestCase("1.0-beta-2", "1.0-beta-10", "1.0-beta-3", ExpectedResult = false)] public bool IsBetween(string versionStr, string lowerStr, string upperStr) { + // arrange ISemanticVersion lower = new SemanticVersion(lowerStr); ISemanticVersion upper = new SemanticVersion(upperStr); ISemanticVersion version = new SemanticVersion(versionStr); + + // assert + Assert.AreEqual(version.IsBetween(lower, upper), version.IsBetween(lower.ToString(), upper.ToString()), "The two signatures returned different results."); return version.IsBetween(lower, upper); } /**** ** Serializable ****/ - [Test(Description = "Assert that SemanticVersion can be round-tripped through JSON with no special configuration.")] + /// Assert that the version can be round-tripped through JSON with no special configuration. + /// The semantic version. [TestCase("1.0.0")] + [TestCase("1.0.0-beta.400")] + [TestCase("1.0.0-beta.400+build")] public void Serializable(string versionStr) { // act @@ -272,10 +368,12 @@ namespace SMAPI.Tests.Utilities Assert.AreEqual(versionStr, after.ToString(), "The semantic version after deserialization doesn't match the input version."); } + /**** ** GameVersion ****/ - [Test(Description = "Assert that the GameVersion subclass correctly parses legacy game versions.")] + /// Assert that the GameVersion subclass correctly parses non-standard game versions. + /// The raw version. [TestCase("1.0")] [TestCase("1.01")] [TestCase("1.02")] @@ -307,6 +405,24 @@ namespace SMAPI.Tests.Utilities /********* ** Private methods *********/ + /// Assert that the version matches the expected parts. + /// The version number. + /// The major number. + /// The minor number. + /// The patch number. + /// The prerelease tag. + /// The build metadata. + /// Whether the version should be marked as non-standard. + private void AssertParts(ISemanticVersion version, int major, int minor, int patch, string prerelease, string build, bool nonStandard) + { + Assert.AreEqual(major, version.MajorVersion, "The major version doesn't match."); + Assert.AreEqual(minor, version.MinorVersion, "The minor version doesn't match."); + Assert.AreEqual(patch, version.PatchVersion, "The patch version doesn't match."); + Assert.AreEqual(string.IsNullOrWhiteSpace(prerelease) ? null : prerelease.Trim(), version.PrereleaseTag, "The prerelease tag doesn't match."); + Assert.AreEqual(string.IsNullOrWhiteSpace(build) ? null : build.Trim(), version.BuildMetadata, "The build metadata doesn't match."); + Assert.AreEqual(nonStandard, version.IsNonStandard(), $"The version is incorrectly marked {(nonStandard ? "standard" : "non-standard")}."); + } + /// Assert that the expected exception type is thrown, and log the action output and thrown exception. /// The expected exception type. /// The action which may throw the exception. diff --git a/src/SMAPI.Toolkit.CoreInterfaces/ISemanticVersion.cs b/src/SMAPI.Toolkit.CoreInterfaces/ISemanticVersion.cs index b8572d50..b228b2d1 100644 --- a/src/SMAPI.Toolkit.CoreInterfaces/ISemanticVersion.cs +++ b/src/SMAPI.Toolkit.CoreInterfaces/ISemanticVersion.cs @@ -61,5 +61,8 @@ namespace StardewModdingAPI /// Get a string representation of the version. string ToString(); + + /// Whether the version uses non-standard extensions, like four-part game versions on some platforms. + bool IsNonStandard(); } } diff --git a/src/SMAPI.Toolkit/Framework/SemanticVersionReader.cs b/src/SMAPI.Toolkit/Framework/SemanticVersionReader.cs new file mode 100644 index 00000000..489e1c4d --- /dev/null +++ b/src/SMAPI.Toolkit/Framework/SemanticVersionReader.cs @@ -0,0 +1,126 @@ +namespace StardewModdingAPI.Toolkit.Framework +{ + /// Reads strings into a semantic version. + internal static class SemanticVersionReader + { + /********* + ** Public methods + *********/ + /// Parse a semantic version string. + /// The version string to parse. + /// Whether to recognize non-standard semver extensions. + /// The major version incremented for major API changes. + /// The minor version incremented for backwards-compatible changes. + /// The patch version for backwards-compatible fixes. + /// The platform-specific version (if applicable). + /// An optional prerelease tag. + /// Optional build metadata. This is ignored when determining version precedence. + /// Returns whether the version was successfully parsed. + public static bool TryParse(string versionStr, bool allowNonStandard, out int major, out int minor, out int patch, out int platformRelease, out string prereleaseTag, out string buildMetadata) + { + // init + major = 0; + minor = 0; + patch = 0; + platformRelease = 0; + prereleaseTag = null; + buildMetadata = null; + + // normalize + versionStr = versionStr?.Trim(); + if (string.IsNullOrWhiteSpace(versionStr)) + return false; + char[] raw = versionStr.ToCharArray(); + + // read major/minor version + int i = 0; + if (!TryParseVersionPart(raw, ref i, out major) || !TryParseLiteral(raw, ref i, '.') || !TryParseVersionPart(raw, ref i, out minor)) + return false; + + // read optional patch version + if (TryParseLiteral(raw, ref i, '.') && !TryParseVersionPart(raw, ref i, out patch)) + return false; + + // read optional non-standard platform release version + if (allowNonStandard && TryParseLiteral(raw, ref i, '.') && !TryParseVersionPart(raw, ref i, out platformRelease)) + return false; + + // read optional prerelease tag + if (TryParseLiteral(raw, ref i, '-') && !TryParseTag(raw, ref i, out prereleaseTag)) + return false; + + // read optional build tag + if (TryParseLiteral(raw, ref i, '+') && !TryParseTag(raw, ref i, out buildMetadata)) + return false; + + // validate + return i == versionStr.Length; // valid if we're at the end + } + + + /********* + ** Private methods + *********/ + /// Try to parse the next characters in a queue as a numeric part. + /// The raw characters to parse. + /// The index of the next character to read. + /// The parsed part. + private static bool TryParseVersionPart(char[] raw, ref int index, out int part) + { + part = 0; + + // take digits + string str = ""; + for (int i = index; i < raw.Length && char.IsDigit(raw[i]); i++) + str += raw[i]; + + // validate + if (str.Length == 0) + return false; + if (str.Length > 1 && str[0] == '0') + return false; // can't have leading zeros + + // parse + part = int.Parse(str); + index += str.Length; + return true; + } + + /// Try to parse a literal character. + /// The raw characters to parse. + /// The index of the next character to read. + /// The expected character. + private static bool TryParseLiteral(char[] raw, ref int index, char ch) + { + if (index >= raw.Length || raw[index] != ch) + return false; + + index++; + return true; + } + + /// Try to parse a tag. + /// The raw characters to parse. + /// The index of the next character to read. + /// The parsed tag. + private static bool TryParseTag(char[] raw, ref int index, out string tag) + { + // read tag length + int length = 0; + for (int i = index; i < raw.Length && (char.IsLetterOrDigit(raw[i]) || raw[i] == '-' || raw[i] == '.'); i++) + length++; + + // validate + if (length == 0) + { + tag = null; + return false; + } + + // parse + tag = new string(raw, index, length); + index += length; + return true; + } + } +} diff --git a/src/SMAPI.Toolkit/SemanticVersion.cs b/src/SMAPI.Toolkit/SemanticVersion.cs index 4955dcae..5ead6dc8 100644 --- a/src/SMAPI.Toolkit/SemanticVersion.cs +++ b/src/SMAPI.Toolkit/SemanticVersion.cs @@ -1,5 +1,6 @@ using System; using System.Text.RegularExpressions; +using StardewModdingAPI.Toolkit.Framework; namespace StardewModdingAPI.Toolkit { @@ -9,6 +10,8 @@ namespace StardewModdingAPI.Toolkit /// - short-form "x.y" versions are supported (equivalent to "x.y.0"); /// - hyphens are synonymous with dots in prerelease tags and build metadata (like "-unofficial.3-pathoschild"); /// - and "-unofficial" in prerelease tags is always lower-precedence (e.g. "1.0-beta" is newer than "1.0-unofficial"). + /// + /// This optionally also supports four-part versions, a non-standard extension used by Stardew Valley on ported platforms to represent platform-specific patches to a ported version, represented as a fourth number in the version string. /// public class SemanticVersion : ISemanticVersion { @@ -16,14 +19,7 @@ namespace StardewModdingAPI.Toolkit ** Fields *********/ /// A regex pattern matching a valid prerelease or build metadata tag. - internal const string TagPattern = @"(?>[a-z0-9]+[\-\.]?)+"; - - /// A regex pattern matching a version within a larger string. - internal const string UnboundedVersionPattern = @"(?>(?0|[1-9]\d*))\.(?>(?0|[1-9]\d*))(?>(?:\.(?0|[1-9]\d*))?)(?:-(?" + SemanticVersion.TagPattern + "))?(?:\\+(?" + SemanticVersion.TagPattern + "))?"; - - /// A regular expression matching a semantic version string. - /// This pattern is derived from the BNF documentation in the semver repo, with deviations to support the Stardew Valley mod conventions (see remarks on ). - internal static readonly Regex Regex = new Regex($@"^{SemanticVersion.UnboundedVersionPattern}$", RegexOptions.CultureInvariant | RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.ExplicitCapture); + private const string TagPattern = @"(?>[a-z0-9]+[\-\.]?)+"; /********* @@ -38,6 +34,9 @@ namespace StardewModdingAPI.Toolkit /// The patch version for backwards-compatible bug fixes. public int PatchVersion { get; } + /// The platform release. This is a non-standard semver extension used by Stardew Valley on ported platforms to represent platform-specific patches to a ported version, represented as a fourth number in the version string. + public int PlatformRelease { get; } + /// An optional prerelease tag. public string PrereleaseTag { get; } @@ -52,13 +51,15 @@ namespace StardewModdingAPI.Toolkit /// The major version incremented for major API changes. /// The minor version incremented for backwards-compatible changes. /// The patch version for backwards-compatible fixes. + /// The platform-specific version (if applicable). /// An optional prerelease tag. /// Optional build metadata. This is ignored when determining version precedence. - public SemanticVersion(int major, int minor, int patch, string prereleaseTag = null, string buildMetadata = null) + public SemanticVersion(int major, int minor, int patch, int platformRelease = 0, string prereleaseTag = null, string buildMetadata = null) { this.MajorVersion = major; this.MinorVersion = minor; this.PatchVersion = patch; + this.PlatformRelease = platformRelease; this.PrereleaseTag = this.GetNormalizedTag(prereleaseTag); this.BuildMetadata = this.GetNormalizedTag(buildMetadata); @@ -82,23 +83,22 @@ namespace StardewModdingAPI.Toolkit /// Construct an instance. /// The semantic version string. + /// Whether to recognize non-standard semver extensions. /// The is null. /// The is not a valid semantic version. - public SemanticVersion(string version) + public SemanticVersion(string version, bool allowNonStandard = false) { - // parse if (version == null) throw new ArgumentNullException(nameof(version), "The input version string can't be null."); - var match = SemanticVersion.Regex.Match(version.Trim()); - if (!match.Success) + if (!SemanticVersionReader.TryParse(version, allowNonStandard, out int major, out int minor, out int patch, out int platformRelease, out string prereleaseTag, out string buildMetadata) || (!allowNonStandard && platformRelease != 0)) throw new FormatException($"The input '{version}' isn't a valid semantic version."); - // initialize - this.MajorVersion = int.Parse(match.Groups["major"].Value); - this.MinorVersion = match.Groups["minor"].Success ? int.Parse(match.Groups["minor"].Value) : 0; - this.PatchVersion = match.Groups["patch"].Success ? int.Parse(match.Groups["patch"].Value) : 0; - this.PrereleaseTag = match.Groups["prerelease"].Success ? this.GetNormalizedTag(match.Groups["prerelease"].Value) : null; - this.BuildMetadata = match.Groups["buildmetadata"].Success ? this.GetNormalizedTag(match.Groups["buildmetadata"].Value) : null; + this.MajorVersion = major; + this.MinorVersion = minor; + this.PatchVersion = patch; + this.PlatformRelease = platformRelease; + this.PrereleaseTag = prereleaseTag; + this.BuildMetadata = buildMetadata; this.AssertValid(); } @@ -110,7 +110,7 @@ namespace StardewModdingAPI.Toolkit { if (other == null) throw new ArgumentNullException(nameof(other)); - return this.CompareTo(other.MajorVersion, other.MinorVersion, other.PatchVersion, other.PrereleaseTag); + return this.CompareTo(other.MajorVersion, other.MinorVersion, other.PatchVersion, (other as SemanticVersion)?.PlatformRelease ?? 0, other.PrereleaseTag); } /// Indicates whether the current object is equal to another object of the same type. @@ -139,7 +139,7 @@ namespace StardewModdingAPI.Toolkit /// The specified version is not a valid semantic version. public bool IsOlderThan(string other) { - return this.IsOlderThan(new SemanticVersion(other)); + return this.IsOlderThan(new SemanticVersion(other, allowNonStandard: true)); } /// Get whether this version is newer than the specified version. @@ -154,7 +154,7 @@ namespace StardewModdingAPI.Toolkit /// The specified version is not a valid semantic version. public bool IsNewerThan(string other) { - return this.IsNewerThan(new SemanticVersion(other)); + return this.IsNewerThan(new SemanticVersion(other, allowNonStandard: true)); } /// Get whether this version is between two specified versions (inclusively). @@ -171,13 +171,15 @@ namespace StardewModdingAPI.Toolkit /// One of the specified versions is not a valid semantic version. public bool IsBetween(string min, string max) { - return this.IsBetween(new SemanticVersion(min), new SemanticVersion(max)); + return this.IsBetween(new SemanticVersion(min, allowNonStandard: true), new SemanticVersion(max, allowNonStandard: true)); } /// Get a string representation of the version. public override string ToString() { string version = $"{this.MajorVersion}.{this.MinorVersion}.{this.PatchVersion}"; + if (this.PlatformRelease != 0) + version += $".{this.PlatformRelease}"; if (this.PrereleaseTag != null) version += $"-{this.PrereleaseTag}"; if (this.BuildMetadata != null) @@ -185,15 +187,30 @@ namespace StardewModdingAPI.Toolkit return version; } + /// Whether the version uses non-standard extensions, like four-part game versions on some platforms. + public bool IsNonStandard() + { + return this.PlatformRelease != 0; + } + /// Parse a version string without throwing an exception if it fails. /// The version string. /// The parsed representation. /// Returns whether parsing the version succeeded. public static bool TryParse(string version, out ISemanticVersion parsed) + { + return SemanticVersion.TryParseNonStandard(version, out parsed) && !parsed.IsNonStandard(); + } + + /// Parse a version string without throwing an exception if it fails, including support for non-standard extensions like . + /// The version string. + /// The parsed representation. + /// Returns whether parsing the version succeeded. + public static bool TryParseNonStandard(string version, out ISemanticVersion parsed) { try { - parsed = new SemanticVersion(version); + parsed = new SemanticVersion(version, true); return true; } catch @@ -219,8 +236,9 @@ namespace StardewModdingAPI.Toolkit /// The major version to compare with this instance. /// The minor version to compare with this instance. /// The patch version to compare with this instance. + /// The non-standard platform release to compare with this instance. /// The prerelease tag to compare with this instance. - private int CompareTo(int otherMajor, int otherMinor, int otherPatch, string otherTag) + private int CompareTo(int otherMajor, int otherMinor, int otherPatch, int otherPlatformRelease, string otherTag) { const int same = 0; const int curNewer = 1; @@ -233,6 +251,8 @@ namespace StardewModdingAPI.Toolkit return this.MinorVersion.CompareTo(otherMinor); if (this.PatchVersion != otherPatch) return this.PatchVersion.CompareTo(otherPatch); + if (this.PlatformRelease != otherPlatformRelease) + return this.PlatformRelease.CompareTo(otherPlatformRelease); if (this.PrereleaseTag == otherTag) return same; @@ -274,7 +294,7 @@ namespace StardewModdingAPI.Toolkit } // fallback (this should never happen) - return string.Compare(this.ToString(), new SemanticVersion(otherMajor, otherMinor, otherPatch, otherTag).ToString(), StringComparison.InvariantCultureIgnoreCase); + return string.Compare(this.ToString(), new SemanticVersion(otherMajor, otherMinor, otherPatch, otherPlatformRelease, otherTag).ToString(), StringComparison.InvariantCultureIgnoreCase); } /// Assert that the current version is valid. diff --git a/src/SMAPI.Toolkit/Serialization/Converters/SemanticVersionConverter.cs b/src/SMAPI.Toolkit/Serialization/Converters/SemanticVersionConverter.cs index ece4a72e..e1b9db1d 100644 --- a/src/SMAPI.Toolkit/Serialization/Converters/SemanticVersionConverter.cs +++ b/src/SMAPI.Toolkit/Serialization/Converters/SemanticVersionConverter.cs @@ -68,7 +68,7 @@ namespace StardewModdingAPI.Toolkit.Serialization.Converters int patch = obj.ValueIgnoreCase(nameof(ISemanticVersion.PatchVersion)); string prereleaseTag = obj.ValueIgnoreCase(nameof(ISemanticVersion.PrereleaseTag)); - return new SemanticVersion(major, minor, patch, prereleaseTag); + return new SemanticVersion(major, minor, patch, prereleaseTag: prereleaseTag); } /// Read a JSON string. diff --git a/src/SMAPI.Web/Framework/LogParsing/LogParser.cs b/src/SMAPI.Web/Framework/LogParsing/LogParser.cs index 1210f708..cc91ec51 100644 --- a/src/SMAPI.Web/Framework/LogParsing/LogParser.cs +++ b/src/SMAPI.Web/Framework/LogParsing/LogParser.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Text.RegularExpressions; -using StardewModdingAPI.Toolkit; using StardewModdingAPI.Web.Framework.LogParsing.Models; namespace StardewModdingAPI.Web.Framework.LogParsing @@ -31,22 +30,22 @@ namespace StardewModdingAPI.Web.Framework.LogParsing /// A regex pattern matching an entry in SMAPI's mod list. /// The author name and description are optional. - private readonly Regex ModListEntryPattern = new Regex(@"^ (?.+?) (?" + SemanticVersion.UnboundedVersionPattern + @")(?: by (?[^\|]+))?(?: \| (?.+))?$", RegexOptions.Compiled | RegexOptions.IgnoreCase); + private readonly Regex ModListEntryPattern = new Regex(@"^ (?.+?) (?[^\s]+)(?: by (?[^\|]+))?(?: \| (?.+))?$", RegexOptions.Compiled | RegexOptions.IgnoreCase); /// A regex pattern matching the start of SMAPI's content pack list. private readonly Regex ContentPackListStartPattern = new Regex(@"^Loaded \d+ content packs:$", RegexOptions.Compiled | RegexOptions.IgnoreCase); /// A regex pattern matching an entry in SMAPI's content pack list. - private readonly Regex ContentPackListEntryPattern = new Regex(@"^ (?.+?) (?" + SemanticVersion.UnboundedVersionPattern + @")(?: by (?[^\|]+))? \| for (?[^\|]+)(?: \| (?.+))?$", RegexOptions.Compiled | RegexOptions.IgnoreCase); + private readonly Regex ContentPackListEntryPattern = new Regex(@"^ (?.+?) (?[^\s]+)(?: by (?[^\|]+))? \| for (?[^\|]+)(?: \| (?.+))?$", RegexOptions.Compiled | RegexOptions.IgnoreCase); /// A regex pattern matching the start of SMAPI's mod update list. private readonly Regex ModUpdateListStartPattern = new Regex(@"^You can update \d+ mods?:$", RegexOptions.Compiled | RegexOptions.IgnoreCase); /// A regex pattern matching an entry in SMAPI's mod update list. - private readonly Regex ModUpdateListEntryPattern = new Regex(@"^ (?.+?) (?" + SemanticVersion.UnboundedVersionPattern + @"): (?.+)$", RegexOptions.Compiled | RegexOptions.IgnoreCase); + private readonly Regex ModUpdateListEntryPattern = new Regex(@"^ (?.+?) (?[^\s]+): (?.+)$", RegexOptions.Compiled | RegexOptions.IgnoreCase); /// A regex pattern matching SMAPI's update line. - private readonly Regex SMAPIUpdatePattern = new Regex(@"^You can update SMAPI to (?" + SemanticVersion.UnboundedVersionPattern + @"): (?.+)$", RegexOptions.Compiled | RegexOptions.IgnoreCase); + private readonly Regex SMAPIUpdatePattern = new Regex(@"^You can update SMAPI to (?[^\s]+): (?.+)$", RegexOptions.Compiled | RegexOptions.IgnoreCase); /********* diff --git a/src/SMAPI.Web/Framework/VersionConstraint.cs b/src/SMAPI.Web/Framework/VersionConstraint.cs index 2d6ec603..72f5ef84 100644 --- a/src/SMAPI.Web/Framework/VersionConstraint.cs +++ b/src/SMAPI.Web/Framework/VersionConstraint.cs @@ -1,16 +1,34 @@ -using Microsoft.AspNetCore.Routing.Constraints; +using System; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; using StardewModdingAPI.Toolkit; namespace StardewModdingAPI.Web.Framework { /// Constrains a route value to a valid semantic version. - internal class VersionConstraint : RegexRouteConstraint + internal class VersionConstraint : IRouteConstraint { /********* ** Public methods *********/ - /// Construct an instance. - public VersionConstraint() - : base(SemanticVersion.Regex) { } + /// Get whether the URL parameter contains a valid value for this constraint. + /// An object that encapsulates information about the HTTP request. + /// The router that this constraint belongs to. + /// The name of the parameter that is being checked. + /// A dictionary that contains the parameters for the URL. + /// An object that indicates whether the constraint check is being performed when an incoming request is being handled or when a URL is being generated. + /// true if the URL parameter contains a valid value; otherwise, false. + public bool Match(HttpContext httpContext, IRouter route, string routeKey, RouteValueDictionary values, RouteDirection routeDirection) + { + if (routeKey == null) + throw new ArgumentNullException(nameof(routeKey)); + if (values == null) + throw new ArgumentNullException(nameof(values)); + + return + values.TryGetValue(routeKey, out object routeValue) + && routeValue is string routeStr + && SemanticVersion.TryParseNonStandard(routeStr, out _); + } } } diff --git a/src/SMAPI/Framework/GameVersion.cs b/src/SMAPI/Framework/GameVersion.cs index 29cfbc39..07957624 100644 --- a/src/SMAPI/Framework/GameVersion.cs +++ b/src/SMAPI/Framework/GameVersion.cs @@ -3,8 +3,8 @@ using System.Collections.Generic; namespace StardewModdingAPI.Framework { - /// An implementation of that correctly handles the non-semantic versions used by older Stardew Valley releases. - internal class GameVersion : SemanticVersion + /// An extension of that correctly handles non-semantic versions used by Stardew Valley. + internal class GameVersion : Toolkit.SemanticVersion { /********* ** Private methods @@ -18,11 +18,11 @@ namespace StardewModdingAPI.Framework ["1.03"] = "1.0.3", ["1.04"] = "1.0.4", ["1.05"] = "1.0.5", - ["1.051"] = "1.0.6-prerelease1", // not a very good mapping, but good enough for SMAPI's purposes. - ["1.051b"] = "1.0.6-prerelease2", + ["1.051"] = "1.0.5.1", + ["1.051b"] = "1.0.5.2", ["1.06"] = "1.0.6", ["1.07"] = "1.0.7", - ["1.07a"] = "1.0.8-prerelease1", + ["1.07a"] = "1.0.7.1", ["1.08"] = "1.0.8", ["1.1"] = "1.1.0", ["1.2"] = "1.2.0", @@ -36,7 +36,7 @@ namespace StardewModdingAPI.Framework /// Construct an instance. /// The game version string. public GameVersion(string version) - : base(GameVersion.GetSemanticVersionString(version)) { } + : base(GameVersion.GetSemanticVersionString(version), allowNonStandard: true) { } /// Get a string representation of the version. public override string ToString() @@ -53,33 +53,21 @@ namespace StardewModdingAPI.Framework private static string GetSemanticVersionString(string gameVersion) { // mapped version - if (GameVersion.VersionMap.TryGetValue(gameVersion, out string semanticVersion)) - return semanticVersion; - - // special case: four-part versions - string[] parts = gameVersion.Split('.'); - if (parts.Length == 4) - return $"{parts[0]}.{parts[1]}.{parts[2]}+{parts[3]}"; - - return gameVersion; + return GameVersion.VersionMap.TryGetValue(gameVersion, out string semanticVersion) + ? semanticVersion + : gameVersion; } /// Convert a semantic version string to the equivalent game version string. /// The semantic version string. private static string GetGameVersionString(string semanticVersion) { - // mapped versions foreach (var mapping in GameVersion.VersionMap) { if (mapping.Value.Equals(semanticVersion, StringComparison.InvariantCultureIgnoreCase)) return mapping.Key; } - // special case: four-part versions - string[] parts = semanticVersion.Split('.', '+'); - if (parts.Length == 4) - return $"{parts[0]}.{parts[1]}.{parts[2]}.{parts[3]}"; - return semanticVersion; } } diff --git a/src/SMAPI/SemanticVersion.cs b/src/SMAPI/SemanticVersion.cs index 2a33ecef..4a175efe 100644 --- a/src/SMAPI/SemanticVersion.cs +++ b/src/SMAPI/SemanticVersion.cs @@ -39,18 +39,36 @@ namespace StardewModdingAPI /// The major version incremented for major API changes. /// The minor version incremented for backwards-compatible changes. /// The patch version for backwards-compatible bug fixes. - /// An optional prerelease tag. - /// Optional build metadata. This is ignored when determining version precedence. + /// An optional prerelease tag. + /// Optional build metadata. This is ignored when determining version precedence. + public SemanticVersion(int majorVersion, int minorVersion, int patchVersion, string prereleaseTag = null, string buildMetadata = null) + : this(majorVersion, minorVersion, patchVersion, 0, prereleaseTag, buildMetadata) { } + + /// Construct an instance. + /// The major version incremented for major API changes. + /// The minor version incremented for backwards-compatible changes. + /// The patch version for backwards-compatible bug fixes. + /// An optional prerelease tag. + /// The platform-specific version (if applicable). + /// Optional build metadata. This is ignored when determining version precedence. [JsonConstructor] - public SemanticVersion(int majorVersion, int minorVersion, int patchVersion, string prerelease = null, string build = null) - : this(new Toolkit.SemanticVersion(majorVersion, minorVersion, patchVersion, prerelease, build)) { } + internal SemanticVersion(int majorVersion, int minorVersion, int patchVersion, int platformRelease, string prereleaseTag = null, string buildMetadata = null) + : this(new Toolkit.SemanticVersion(majorVersion, minorVersion, patchVersion, platformRelease, prereleaseTag, buildMetadata)) { } /// Construct an instance. /// The semantic version string. /// The is null. /// The is not a valid semantic version. public SemanticVersion(string version) - : this(new Toolkit.SemanticVersion(version)) { } + : this(version, allowNonStandard: false) { } + + /// Construct an instance. + /// The semantic version string. + /// Whether to recognize non-standard semver extensions. + /// The is null. + /// The is not a valid semantic version. + internal SemanticVersion(string version, bool allowNonStandard) + : this(new Toolkit.SemanticVersion(version, allowNonStandard)) { } /// Construct an instance. /// The assembly version. @@ -141,6 +159,12 @@ namespace StardewModdingAPI return this.Version.ToString(); } + /// Whether the version uses non-standard extensions, like four-part game versions on some platforms. + public bool IsNonStandard() + { + return this.Version.IsNonStandard(); + } + /// Parse a version string without throwing an exception if it fails. /// The version string. /// The parsed representation. -- cgit From e33386abcc03c6fd94c365309a23e66f0fe9d6eb Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 25 Jan 2020 11:00:37 -0500 Subject: prevent load crashes due to invalid building types --- docs/release-notes.md | 1 + src/SMAPI/Patches/LoadErrorPatch.cs | 87 +++++++++++++++++++++++++++++-------- 2 files changed, 71 insertions(+), 17 deletions(-) (limited to 'docs') diff --git a/docs/release-notes.md b/docs/release-notes.md index 35fdbb4b..970f26f1 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -5,6 +5,7 @@ * For players: * SMAPI now prevents mods from crashing the game with invalid schedule data. + * SMAPI now prevents load crashes due to invalid building types. * Updated minimum game version (1.4 → 1.4.1). * Fixed 'collection was modified' error when returning to title in rare cases. * Fixed update-check error if a mod's Chucklefish page has no version. diff --git a/src/SMAPI/Patches/LoadErrorPatch.cs b/src/SMAPI/Patches/LoadErrorPatch.cs index eedb4164..c16ca7cc 100644 --- a/src/SMAPI/Patches/LoadErrorPatch.cs +++ b/src/SMAPI/Patches/LoadErrorPatch.cs @@ -3,8 +3,10 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; using Harmony; +using StardewModdingAPI.Framework.Exceptions; using StardewModdingAPI.Framework.Patching; using StardewValley; +using StardewValley.Buildings; using StardewValley.Locations; namespace StardewModdingAPI.Patches @@ -63,11 +65,25 @@ namespace StardewModdingAPI.Patches /// The game locations being loaded. /// Returns whether to execute the original method. private static bool Before_SaveGame_LoadDataToLocations(List gamelocations) + { + bool removedAny = + LoadErrorPatch.RemoveInvalidLocations(gamelocations) + | LoadErrorPatch.RemoveBrokenBuildings(gamelocations) + | LoadErrorPatch.RemoveInvalidNpcs(gamelocations); + + if (removedAny) + LoadErrorPatch.OnContentRemoved(); + + return true; + } + + /// Remove locations which don't exist in-game. + /// The current game locations. + private static bool RemoveInvalidLocations(List locations) { bool removedAny = false; - // remove invalid locations - foreach (GameLocation location in gamelocations.ToArray()) + foreach (GameLocation location in locations.ToArray()) { if (location is Cellar) continue; // missing cellars will be added by the game code @@ -75,23 +91,48 @@ namespace StardewModdingAPI.Patches if (Game1.getLocationFromName(location.name) == null) { LoadErrorPatch.Monitor.Log($"Removed invalid location '{location.Name}' to avoid a crash when loading save '{Constants.SaveFolderName}'. (Did you remove a custom location mod?)", LogLevel.Warn); - gamelocations.Remove(location); + locations.Remove(location); removedAny = true; } } - // get building interiors - var interiors = - ( - from location in gamelocations.OfType() - from building in location.buildings - where building.indoors.Value != null - select building.indoors.Value - ); + return removedAny; + } + + /// Remove buildings which don't exist in the game data. + /// The current game locations. + private static bool RemoveBrokenBuildings(IEnumerable locations) + { + bool removedAny = false; + + foreach (BuildableGameLocation location in locations.OfType()) + { + foreach (Building building in location.buildings.ToArray()) + { + try + { + BluePrint _ = new BluePrint(building.buildingType.Value); + } + catch (SContentLoadException) + { + LoadErrorPatch.Monitor.Log($"Removed invalid building type '{building.buildingType.Value}' in {location.Name} ({building.tileX}, {building.tileY}) to avoid a crash when loading save '{Constants.SaveFolderName}'. (Did you remove a custom building mod?)", LogLevel.Warn); + location.buildings.Remove(building); + removedAny = true; + } + } + } + + return removedAny; + } + + /// Remove NPCs which don't exist in the game data. + /// The current game locations. + private static bool RemoveInvalidNpcs(IEnumerable locations) + { + bool removedAny = false; - // remove custom NPCs which no longer exist IDictionary data = Game1.content.Load>("Data\\NPCDispositions"); - foreach (GameLocation location in gamelocations.Concat(interiors)) + foreach (GameLocation location in LoadErrorPatch.GetAllLocations(locations)) { foreach (NPC npc in location.characters.ToArray()) { @@ -103,7 +144,7 @@ namespace StardewModdingAPI.Patches } catch { - LoadErrorPatch.Monitor.Log($"Removed invalid villager '{npc.Name}' to avoid a crash when loading save '{Constants.SaveFolderName}'. (Did you remove a custom NPC mod?)", LogLevel.Warn); + LoadErrorPatch.Monitor.Log($"Removed invalid villager '{npc.Name}' in {location.Name} ({npc.getTileLocation()}) to avoid a crash when loading save '{Constants.SaveFolderName}'. (Did you remove a custom NPC mod?)", LogLevel.Warn); location.characters.Remove(npc); removedAny = true; } @@ -111,10 +152,22 @@ namespace StardewModdingAPI.Patches } } - if (removedAny) - LoadErrorPatch.OnContentRemoved(); + return removedAny; + } - return true; + /// Get all locations, including building interiors. + /// The main game locations. + private static IEnumerable GetAllLocations(IEnumerable locations) + { + foreach (GameLocation location in locations) + { + yield return location; + if (location is BuildableGameLocation buildableLocation) + { + foreach (GameLocation interior in buildableLocation.buildings.Select(p => p.indoors.Value).Where(p => p != null)) + yield return interior; + } + } } } } -- cgit From 4db7ca28f68be2e25b565a1b98e2c05fb42a5a88 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 25 Jan 2020 11:05:36 -0500 Subject: fix error building/demolishing buildings for some players --- docs/release-notes.md | 1 + src/SMAPI/Framework/SnapshotListDiff.cs | 6 ++++-- 2 files changed, 5 insertions(+), 2 deletions(-) (limited to 'docs') diff --git a/docs/release-notes.md b/docs/release-notes.md index 970f26f1..d8ff8f6a 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -10,6 +10,7 @@ * Fixed 'collection was modified' error when returning to title in rare cases. * Fixed update-check error if a mod's Chucklefish page has no version. * Fixed SMAPI beta versions not showing update alert on next launch (thanks to danvolchek!). + * Fixed rare error when building/demolishing buildings. * For the Console Commands mod: * Added `test_input` command to view button codes in the console. diff --git a/src/SMAPI/Framework/SnapshotListDiff.cs b/src/SMAPI/Framework/SnapshotListDiff.cs index d4d5df50..2d0efa0d 100644 --- a/src/SMAPI/Framework/SnapshotListDiff.cs +++ b/src/SMAPI/Framework/SnapshotListDiff.cs @@ -42,10 +42,12 @@ namespace StardewModdingAPI.Framework this.IsChanged = isChanged; this.RemovedImpl.Clear(); - this.RemovedImpl.AddRange(removed); + if (removed != null) + this.RemovedImpl.AddRange(removed); this.AddedImpl.Clear(); - this.AddedImpl.AddRange(added); + if (added != null) + this.AddedImpl.AddRange(added); } /// Update the snapshot. -- cgit From 7a6dab7548e7bc32a685e916edec83f6458881c1 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 25 Jan 2020 15:21:40 -0500 Subject: fix dialogue asset propagation --- docs/release-notes.md | 1 + src/SMAPI/Metadata/CoreAssetPropagator.cs | 4 ++++ 2 files changed, 5 insertions(+) (limited to 'docs') diff --git a/docs/release-notes.md b/docs/release-notes.md index d8ff8f6a..fdae4dc5 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -19,6 +19,7 @@ * Asset propagation for player sprites now affects other players' sprites, and updates recolor maps (e.g. sleeves). * Removed invalid-schedule validation which had false positives. * Fixed `helper.Data.Read/WriteGlobalData` using the `Saves` folder. The installer will move existing folders to the appdata folder. + * Fixed dialogue asset changes not correctly propagated until the next day. * For SMAPI/tool developers: * Added internal support for four-part versions to support SMAPI on Android. diff --git a/src/SMAPI/Metadata/CoreAssetPropagator.cs b/src/SMAPI/Metadata/CoreAssetPropagator.cs index 57e1d197..7a58d52c 100644 --- a/src/SMAPI/Metadata/CoreAssetPropagator.cs +++ b/src/SMAPI/Metadata/CoreAssetPropagator.cs @@ -887,7 +887,11 @@ namespace StardewModdingAPI.Metadata // update dialogue foreach (NPC villager in villagers) + { villager.resetSeasonalDialogue(); // doesn't only affect seasonal dialogue + villager.resetCurrentDialogue(); + } + return true; } -- cgit From fc0b98be4584eec2b06a3af09cc4b85f9e9a9efc Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 25 Jan 2020 18:22:50 -0500 Subject: add user settings that override defaults (#693) --- docs/release-notes.md | 1 + docs/technical/smapi.md | 13 ++----------- src/SMAPI/Constants.cs | 3 +++ src/SMAPI/Framework/SCore.cs | 3 +++ src/SMAPI/SMAPI.config.json | 6 ++++++ 5 files changed, 15 insertions(+), 11 deletions(-) (limited to 'docs') diff --git a/docs/release-notes.md b/docs/release-notes.md index fdae4dc5..dc26db2d 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -6,6 +6,7 @@ * For players: * SMAPI now prevents mods from crashing the game with invalid schedule data. * SMAPI now prevents load crashes due to invalid building types. + * Added support for persistent `smapi-internal/config.json` overrides (see info in the file). * Updated minimum game version (1.4 → 1.4.1). * Fixed 'collection was modified' error when returning to title in rare cases. * Fixed update-check error if a mod's Chucklefish page has no version. diff --git a/docs/technical/smapi.md b/docs/technical/smapi.md index d565aeb4..c9d5c07e 100644 --- a/docs/technical/smapi.md +++ b/docs/technical/smapi.md @@ -19,17 +19,8 @@ This document is about SMAPI itself; see also [mod build package](mod-package.md ## Customisation ### Configuration file -You can customise the SMAPI behaviour by editing the `smapi-internal/config.json` file in your game -folder. - -Basic fields: - -field | purpose ------------------ | ------- -`DeveloperMode` | Default `false` (except in _SMAPI for developers_ releases). Whether to enable features intended for mod developers (mainly more detailed console logging). -`CheckForUpdates` | Default `true`. Whether SMAPI should check for a newer version when you load the game. If a new version is available, a small message will appear in the console. This doesn't affect the load time even if your connection is offline or slow, because it happens in the background. -`VerboseLogging` | Default `false`. Whether SMAPI should log more information about the game context. -`ModData` | Internal metadata about SMAPI mods. Changing this isn't recommended and may destabilise your game. See documentation in the file. +You can customise some SMAPI behaviour by editing the `smapi-internal/config.json` file in your +game folder. See documentation in the file for more info. ### Command-line arguments The SMAPI installer recognises three command-line arguments: diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs index da2ee375..d2af5de2 100644 --- a/src/SMAPI/Constants.cs +++ b/src/SMAPI/Constants.cs @@ -61,6 +61,9 @@ namespace StardewModdingAPI /// The file path for the SMAPI configuration file. internal static string ApiConfigPath => Path.Combine(Constants.InternalFilesPath, "config.json"); + /// The file path for the overrides file for , which is applied over it. + internal static string ApiUserConfigPath => Path.Combine(Constants.InternalFilesPath, "config.user.json"); + /// The file path for the SMAPI metadata file. internal static string ApiMetadataPath => Path.Combine(Constants.InternalFilesPath, "metadata.json"); diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index d71b5e5a..81b7c2e8 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -153,6 +153,9 @@ namespace StardewModdingAPI.Framework // init basics this.Settings = JsonConvert.DeserializeObject(File.ReadAllText(Constants.ApiConfigPath)); + if (File.Exists(Constants.ApiUserConfigPath)) + JsonConvert.PopulateObject(File.ReadAllText(Constants.ApiUserConfigPath), this.Settings); + this.LogFile = new LogFileManager(logPath); this.Monitor = new Monitor("SMAPI", this.ConsoleManager, this.LogFile, this.Settings.ConsoleColors, this.Settings.VerboseLogging) { diff --git a/src/SMAPI/SMAPI.config.json b/src/SMAPI/SMAPI.config.json index 824bb783..57b4f885 100644 --- a/src/SMAPI/SMAPI.config.json +++ b/src/SMAPI/SMAPI.config.json @@ -6,6 +6,12 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha The default values are mirrored in StardewModdingAPI.Framework.Models.SConfig to log custom changes. +This file is overwritten each time you update or reinstall SMAPI. To avoid losing custom settings, +create a 'config.user.json' file in the same folder with *only* the settings you want to change. +That file won't be overwritten, and any settings in it will override the default options. Don't +copy all the settings, or you may cause bugs due to overridden changes in future SMAPI versions. + + */ { -- cgit From 9f36b2b3d69ee0a45241bfcc45953df29f167aef Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 26 Jan 2020 20:48:29 -0500 Subject: update release notes --- docs/release-notes.md | 2 ++ 1 file changed, 2 insertions(+) (limited to 'docs') diff --git a/docs/release-notes.md b/docs/release-notes.md index dc26db2d..5379950e 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -14,6 +14,7 @@ * Fixed rare error when building/demolishing buildings. * For the Console Commands mod: + * Added `performance` command to track mod performance metrics. This is an advanced experimental feature. (Thanks to Drachenkätzchen!) * Added `test_input` command to view button codes in the console. * For modders: @@ -23,6 +24,7 @@ * Fixed dialogue asset changes not correctly propagated until the next day. * For SMAPI/tool developers: + * Added internal performance monitoring (thanks to Drachenkätzchen!). This is disabled by default in the current version, but can be enabled using the `performance` console command. * Added internal support for four-part versions to support SMAPI on Android. * Updated links for the new r/SMAPI subreddit. * The `/mods` web API endpoint now includes version mappings from the wiki. -- cgit From e5d8acf240f923a09bdaad3fb14b2c34847860dc Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 30 Jan 2020 22:10:16 -0500 Subject: rework asset editor/loader tracking so they're affected by load order --- docs/release-notes.md | 2 ++ src/SMAPI/Framework/ContentCoordinator.cs | 4 +-- .../ContentManagers/GameContentManager.cs | 36 +++++++--------------- src/SMAPI/Framework/ModLinked.cs | 29 +++++++++++++++++ src/SMAPI/Framework/SCore.cs | 26 +++++++++++++--- 5 files changed, 66 insertions(+), 31 deletions(-) create mode 100644 src/SMAPI/Framework/ModLinked.cs (limited to 'docs') diff --git a/docs/release-notes.md b/docs/release-notes.md index 5379950e..dada7726 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -19,9 +19,11 @@ * For modders: * Asset propagation for player sprites now affects other players' sprites, and updates recolor maps (e.g. sleeves). + * Reworked the order that asset editors/loaders are called between multiple mods to support some framework mods like Content Patcher and Json Assets. Note that the order is undefined and should not be depended on. * Removed invalid-schedule validation which had false positives. * Fixed `helper.Data.Read/WriteGlobalData` using the `Saves` folder. The installer will move existing folders to the appdata folder. * Fixed dialogue asset changes not correctly propagated until the next day. + * Fixed issue where a mod which implemented `IAssetEditor`/`IAssetLoader` on its entry class could then remove itself from the editor/loader list. * For SMAPI/tool developers: * Added internal performance monitoring (thanks to Drachenkätzchen!). This is disabled by default in the current version, but can be enabled using the `performance` console command. diff --git a/src/SMAPI/Framework/ContentCoordinator.cs b/src/SMAPI/Framework/ContentCoordinator.cs index b60483f1..2fd31263 100644 --- a/src/SMAPI/Framework/ContentCoordinator.cs +++ b/src/SMAPI/Framework/ContentCoordinator.cs @@ -65,10 +65,10 @@ namespace StardewModdingAPI.Framework public LocalizedContentManager.LanguageCode Language => this.MainContentManager.Language; /// Interceptors which provide the initial versions of matching assets. - public IDictionary> Loaders { get; } = new Dictionary>(); + public IList> Loaders { get; } = new List>(); /// Interceptors which edit matching assets after they're loaded. - public IDictionary> Editors { get; } = new Dictionary>(); + public IList> Editors { get; } = new List>(); /// The absolute path to the . public string FullRootDirectory { get; } diff --git a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs index 8930267d..eecdda74 100644 --- a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs @@ -21,10 +21,10 @@ namespace StardewModdingAPI.Framework.ContentManagers private readonly ContextHash AssetsBeingLoaded = new ContextHash(); /// Interceptors which provide the initial versions of matching assets. - private IDictionary> Loaders => this.Coordinator.Loaders; + private IList> Loaders => this.Coordinator.Loaders; /// Interceptors which edit matching assets after they're loaded. - private IDictionary> Editors => this.Coordinator.Editors; + private IList> Editors => this.Coordinator.Editors; /// A lookup which indicates whether the asset is localizable (i.e. the filename contains the locale), if previously loaded. private readonly IDictionary IsLocalizableLookup; @@ -278,16 +278,16 @@ namespace StardewModdingAPI.Framework.ContentManagers private IAssetData ApplyLoader(IAssetInfo info) { // find matching loaders - var loaders = this.GetInterceptors(this.Loaders) + var loaders = this.Loaders .Where(entry => { try { - return entry.Value.CanLoad(info); + return entry.Data.CanLoad(info); } catch (Exception ex) { - entry.Key.LogAsMod($"Mod failed when checking whether it could load asset '{info.AssetName}', and will be ignored. Error details:\n{ex.GetLogSummary()}", LogLevel.Error); + entry.Mod.LogAsMod($"Mod failed when checking whether it could load asset '{info.AssetName}', and will be ignored. Error details:\n{ex.GetLogSummary()}", LogLevel.Error); return false; } }) @@ -298,14 +298,14 @@ namespace StardewModdingAPI.Framework.ContentManagers return null; if (loaders.Length > 1) { - string[] loaderNames = loaders.Select(p => p.Key.DisplayName).ToArray(); + string[] loaderNames = loaders.Select(p => p.Mod.DisplayName).ToArray(); this.Monitor.Log($"Multiple mods want to provide the '{info.AssetName}' asset ({string.Join(", ", loaderNames)}), but an asset can't be loaded multiple times. SMAPI will use the default asset instead; uninstall one of the mods to fix this. (Message for modders: you should usually use {typeof(IAssetEditor)} instead to avoid conflicts.)", LogLevel.Warn); return null; } // fetch asset from loader - IModMetadata mod = loaders[0].Key; - IAssetLoader loader = loaders[0].Value; + IModMetadata mod = loaders[0].Mod; + IAssetLoader loader = loaders[0].Data; T data; try { @@ -338,11 +338,11 @@ namespace StardewModdingAPI.Framework.ContentManagers IAssetData GetNewData(object data) => new AssetDataForObject(info, data, this.AssertAndNormalizeAssetName); // edit asset - foreach (var entry in this.GetInterceptors(this.Editors)) + foreach (var entry in this.Editors) { // check for match - IModMetadata mod = entry.Key; - IAssetEditor editor = entry.Value; + IModMetadata mod = entry.Mod; + IAssetEditor editor = entry.Data; try { if (!editor.CanEdit(info)) @@ -382,19 +382,5 @@ namespace StardewModdingAPI.Framework.ContentManagers // return result return asset; } - - /// Get all registered interceptors from a list. - private IEnumerable> GetInterceptors(IDictionary> entries) - { - foreach (var entry in entries) - { - IModMetadata mod = entry.Key; - IList interceptors = entry.Value; - - // registered editors - foreach (T interceptor in interceptors) - yield return new KeyValuePair(mod, interceptor); - } - } } } diff --git a/src/SMAPI/Framework/ModLinked.cs b/src/SMAPI/Framework/ModLinked.cs new file mode 100644 index 00000000..8cfe6f5f --- /dev/null +++ b/src/SMAPI/Framework/ModLinked.cs @@ -0,0 +1,29 @@ +namespace StardewModdingAPI.Framework +{ + /// A generic tuple which links something to a mod. + /// The interceptor type. + internal class ModLinked + { + /********* + ** Accessors + *********/ + /// The mod metadata. + public IModMetadata Mod { get; } + + /// The instance linked to the mod. + public T Data { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The mod metadata. + /// The instance linked to the mod. + public ModLinked(IModMetadata mod, T data) + { + this.Mod = mod; + this.Data = data; + } + } +} diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index 9139b371..7e1f8770 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -807,13 +807,13 @@ namespace StardewModdingAPI.Framework { // ReSharper disable SuspiciousTypeConversion.Global if (metadata.Mod is IAssetEditor editor) - helper.ObservableAssetEditors.Add(editor); + this.ContentCore.Editors.Add(new ModLinked(metadata, editor)); if (metadata.Mod is IAssetLoader loader) - helper.ObservableAssetLoaders.Add(loader); + this.ContentCore.Loaders.Add(new ModLinked(metadata, loader)); // ReSharper restore SuspiciousTypeConversion.Global - this.ContentCore.Editors[metadata] = helper.ObservableAssetEditors; - this.ContentCore.Loaders[metadata] = helper.ObservableAssetLoaders; + helper.ObservableAssetEditors.CollectionChanged += (sender, e) => this.OnInterceptorsChanged(metadata, e.NewItems?.Cast(), e.OldItems?.Cast(), this.ContentCore.Editors); + helper.ObservableAssetLoaders.CollectionChanged += (sender, e) => this.OnInterceptorsChanged(metadata, e.NewItems?.Cast(), e.OldItems?.Cast(), this.ContentCore.Loaders); } // call entry method @@ -862,6 +862,24 @@ namespace StardewModdingAPI.Framework this.ModRegistry.AreAllModsInitialized = true; } + /// Handle a mod adding or removing asset interceptors. + /// The asset interceptor type (one of or ). + /// The mod metadata. + /// The interceptors that were added. + /// The interceptors that were removed. + /// The list to update. + private void OnInterceptorsChanged(IModMetadata mod, IEnumerable added, IEnumerable removed, IList> list) + { + foreach (T interceptor in added ?? new T[0]) + list.Add(new ModLinked(mod, interceptor)); + + foreach (T interceptor in removed ?? new T[0]) + { + foreach (ModLinked entry in list.Where(p => p.Mod == mod && object.ReferenceEquals(p.Data, interceptor)).ToArray()) + list.Remove(entry); + } + } + /// Load a given mod. /// The mod to load. /// The mods being loaded. -- cgit From 125b38c6e62d86115045c2cece6c6c3d1da35600 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 1 Feb 2020 00:18:56 -0500 Subject: improve Save Backup compatibility on Android --- docs/release-notes.md | 4 +++ src/SMAPI.Mods.SaveBackup/ModEntry.cs | 50 ++++++++++++++++++++++++----------- 2 files changed, 39 insertions(+), 15 deletions(-) (limited to 'docs') diff --git a/docs/release-notes.md b/docs/release-notes.md index dada7726..b49307c6 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -17,6 +17,10 @@ * Added `performance` command to track mod performance metrics. This is an advanced experimental feature. (Thanks to Drachenkätzchen!) * Added `test_input` command to view button codes in the console. +* For the Save Backup mod: + * Fixed extra files under `Saves` (e.g. manual backups) not being ignored. + * Fixed Android issue where game files were backed up. + * For modders: * Asset propagation for player sprites now affects other players' sprites, and updates recolor maps (e.g. sleeves). * Reworked the order that asset editors/loaders are called between multiple mods to support some framework mods like Content Patcher and Json Assets. Note that the order is undefined and should not be depended on. diff --git a/src/SMAPI.Mods.SaveBackup/ModEntry.cs b/src/SMAPI.Mods.SaveBackup/ModEntry.cs index 3b47759b..b1d368a1 100644 --- a/src/SMAPI.Mods.SaveBackup/ModEntry.cs +++ b/src/SMAPI.Mods.SaveBackup/ModEntry.cs @@ -68,22 +68,21 @@ namespace StardewModdingAPI.Mods.SaveBackup if (targetFile.Exists || fallbackDir.Exists) return; - // back up saves - this.Monitor.Log($"Backing up saves to {targetFile.FullName}...", LogLevel.Trace); - if (!this.TryCompress(Constants.SavesPath, targetFile, out Exception compressError)) + // copy saves to fallback directory (ignore non-save files/folders) + this.Monitor.Log($"Backing up saves to {fallbackDir.FullName}...", LogLevel.Trace); + DirectoryInfo savesDir = new DirectoryInfo(Constants.SavesPath); + this.RecursiveCopy(savesDir, fallbackDir, copyRoot: false, entry => this.MatchSaveFolders(savesDir, entry)); + + // compress backup if possible + this.Monitor.Log("Compressing backup if possible...", LogLevel.Trace); + if (!this.TryCompress(fallbackDir.FullName, targetFile, out Exception compressError)) { - // log error (expected on Android due to missing compression DLLs) - if (Constants.TargetPlatform == GamePlatform.Android) - this.Monitor.VerboseLog($"Compression isn't supported on Android:\n{compressError}"); - else - { - this.Monitor.Log("Couldn't zip the save backup, creating uncompressed backup instead.", LogLevel.Debug); - this.Monitor.Log(compressError.ToString(), LogLevel.Trace); - } - - // fallback to uncompressed - this.RecursiveCopy(new DirectoryInfo(Constants.SavesPath), fallbackDir, copyRoot: false); + if (Constants.TargetPlatform != GamePlatform.Android) // expected to fail on Android + this.Monitor.Log($"Couldn't compress backup, leaving it uncompressed.\n{compressError}", LogLevel.Trace); } + else + fallbackDir.Delete(recursive: true); + this.Monitor.Log("Backup done!", LogLevel.Trace); } catch (Exception ex) @@ -198,12 +197,16 @@ namespace StardewModdingAPI.Mods.SaveBackup /// The file or folder to copy. /// The folder to copy into. /// Whether to copy the root folder itself, or false to only copy its contents. + /// A filter which matches the files or directories to copy, or null to copy everything. /// Derived from the SMAPI installer code. - private void RecursiveCopy(FileSystemInfo source, DirectoryInfo targetFolder, bool copyRoot = true) + private void RecursiveCopy(FileSystemInfo source, DirectoryInfo targetFolder, bool copyRoot = true, Func filter = null) { if (!targetFolder.Exists) targetFolder.Create(); + if (filter?.Invoke(source) == false) + return; + switch (source) { case FileInfo sourceFile: @@ -220,5 +223,22 @@ namespace StardewModdingAPI.Mods.SaveBackup throw new NotSupportedException($"Unknown filesystem info type '{source.GetType().FullName}'."); } } + + /// A copy filter which matches save folders. + /// The folder containing save folders. + /// The current entry to check under . + private bool MatchSaveFolders(DirectoryInfo savesFolder, FileSystemInfo entry) + { + // only need to filter top-level entries + string parentPath = (entry as FileInfo)?.DirectoryName ?? (entry as DirectoryInfo)?.Parent?.FullName; + if (parentPath != savesFolder.FullName) + return true; + + + // match folders with Name_ID format + return + entry is DirectoryInfo + && ulong.TryParse(entry.Name.Split('_').Last(), out _); + } } } -- cgit From c8191449a00e3db08214e3b1146e17f89f0245c5 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 1 Feb 2020 00:27:53 -0500 Subject: add support for *.tmx map files --- build/common.targets | 3 ++- build/prepare-install-package.targets | 1 + docs/release-notes.md | 1 + .../Framework/ContentManagers/ModContentManager.cs | 1 + src/SMAPI/Framework/SCore.cs | 13 ++++++++++- src/SMAPI/SMAPI.csproj | 27 ++-------------------- 6 files changed, 19 insertions(+), 27 deletions(-) (limited to 'docs') diff --git a/build/common.targets b/build/common.targets index df2d4861..cfdcccca 100644 --- a/build/common.targets +++ b/build/common.targets @@ -31,8 +31,9 @@ - + + diff --git a/build/prepare-install-package.targets b/build/prepare-install-package.targets index 790b8bad..7b9d63f9 100644 --- a/build/prepare-install-package.targets +++ b/build/prepare-install-package.targets @@ -45,6 +45,7 @@ + diff --git a/docs/release-notes.md b/docs/release-notes.md index b49307c6..1ad73492 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -22,6 +22,7 @@ * Fixed Android issue where game files were backed up. * For modders: + * Added support for loading `.tmx` map files. * Asset propagation for player sprites now affects other players' sprites, and updates recolor maps (e.g. sleeves). * Reworked the order that asset editors/loaders are called between multiple mods to support some framework mods like Content Patcher and Json Assets. Note that the order is undefined and should not be depended on. * Removed invalid-schedule validation which had false positives. diff --git a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs index fdf76b24..0a526fc8 100644 --- a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs @@ -154,6 +154,7 @@ namespace StardewModdingAPI.Framework.ContentManagers // unpacked map case ".tbin": + case ".tmx": { // validate if (typeof(T) != typeof(Map)) diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index 44c495ff..77c2fab8 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -234,7 +234,7 @@ namespace StardewModdingAPI.Framework #endif AppDomain.CurrentDomain.UnhandledException += (sender, e) => this.Monitor.Log($"Critical app domain exception: {e.ExceptionObject}", LogLevel.Error); - // add more lenient assembly resolvers + // add more lenient assembly resolver AppDomain.CurrentDomain.AssemblyResolve += (sender, e) => AssemblyLoader.ResolveAssembly(e.Name); // hook locale event @@ -420,6 +420,17 @@ namespace StardewModdingAPI.Framework return; } + // init TMX support + try + { + xTile.Format.FormatManager.Instance.RegisterMapFormat(new TMXTile.TMXFormat(Game1.tileSize / Game1.pixelZoom, Game1.tileSize / Game1.pixelZoom, Game1.pixelZoom, Game1.pixelZoom)); + } + catch (Exception ex) + { + this.Monitor.Log("SMAPI couldn't load TMX support. Some mods may not work correctly.", LogLevel.Warn); + this.Monitor.Log($"Technical details: {ex.GetLogSummary()}", LogLevel.Trace); + } + // load mod data ModToolkit toolkit = new ModToolkit(); ModDatabase modDatabase = toolkit.GetModDatabase(Constants.ApiMetadataPath); diff --git a/src/SMAPI/SMAPI.csproj b/src/SMAPI/SMAPI.csproj index 1376c8a2..579af423 100644 --- a/src/SMAPI/SMAPI.csproj +++ b/src/SMAPI/SMAPI.csproj @@ -19,6 +19,7 @@ + @@ -98,31 +99,7 @@ SMAPI.metadata.json PreserveNewest - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - + PreserveNewest -- cgit From 70a1334f2c50cff279344b9b9d52d71c847516a7 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 1 Feb 2020 01:08:29 -0500 Subject: add JSON converter for Vector2 --- docs/release-notes.md | 1 + src/SMAPI/Framework/SCore.cs | 1 + .../Framework/Serialization/PointConverter.cs | 2 +- .../Framework/Serialization/Vector2Converter.cs | 43 ++++++++++++++++++++++ 4 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 src/SMAPI/Framework/Serialization/Vector2Converter.cs (limited to 'docs') diff --git a/docs/release-notes.md b/docs/release-notes.md index 1ad73492..13735e76 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -23,6 +23,7 @@ * For modders: * Added support for loading `.tmx` map files. + * Added JSON converter for `Vector2` values, so they work consistently crossplatform. * Asset propagation for player sprites now affects other players' sprites, and updates recolor maps (e.g. sleeves). * Reworked the order that asset editors/loaders are called between multiple mods to support some framework mods like Content Patcher and Json Assets. Note that the order is undefined and should not be depended on. * Removed invalid-schedule validation which had false positives. diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index 77c2fab8..50e6ea1c 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -222,6 +222,7 @@ namespace StardewModdingAPI.Framework JsonConverter[] converters = { new ColorConverter(), new PointConverter(), + new Vector2Converter(), new RectangleConverter() }; foreach (JsonConverter converter in converters) diff --git a/src/SMAPI/Framework/Serialization/PointConverter.cs b/src/SMAPI/Framework/Serialization/PointConverter.cs index 8c2f3396..3481c9b2 100644 --- a/src/SMAPI/Framework/Serialization/PointConverter.cs +++ b/src/SMAPI/Framework/Serialization/PointConverter.cs @@ -6,7 +6,7 @@ using StardewModdingAPI.Toolkit.Serialization.Converters; namespace StardewModdingAPI.Framework.Serialization { - /// Handles deserialization of for crossplatform compatibility. + /// Handles deserialization of for crossplatform compatibility. /// /// - Linux/Mac format: { "X": 1, "Y": 2 } /// - Windows format: "1, 2" diff --git a/src/SMAPI/Framework/Serialization/Vector2Converter.cs b/src/SMAPI/Framework/Serialization/Vector2Converter.cs new file mode 100644 index 00000000..1d9b08e0 --- /dev/null +++ b/src/SMAPI/Framework/Serialization/Vector2Converter.cs @@ -0,0 +1,43 @@ +using System; +using Microsoft.Xna.Framework; +using Newtonsoft.Json.Linq; +using StardewModdingAPI.Toolkit.Serialization; +using StardewModdingAPI.Toolkit.Serialization.Converters; + +namespace StardewModdingAPI.Framework.Serialization +{ + /// Handles deserialization of for crossplatform compatibility. + /// + /// - Linux/Mac format: { "X": 1, "Y": 2 } + /// - Windows format: "1, 2" + /// + internal class Vector2Converter : SimpleReadOnlyConverter + { + /********* + ** Protected methods + *********/ + /// Read a JSON object. + /// The JSON object to read. + /// The path to the current JSON node. + protected override Vector2 ReadObject(JObject obj, string path) + { + float x = obj.ValueIgnoreCase(nameof(Vector2.X)); + float y = obj.ValueIgnoreCase(nameof(Vector2.Y)); + return new Vector2(x, y); + } + + /// Read a JSON string. + /// The JSON string value. + /// The path to the current JSON node. + protected override Vector2 ReadString(string str, string path) + { + string[] parts = str.Split(','); + if (parts.Length != 2) + throw new SParseException($"Can't parse {typeof(Vector2).Name} from invalid value '{str}' (path: {path})."); + + float x = Convert.ToSingle(parts[0]); + float y = Convert.ToSingle(parts[1]); + return new Vector2(x, y); + } + } +} -- cgit From 0fddc7f5109bffe2912e9fdcf5ed398d80c25dec Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 1 Feb 2020 01:11:46 -0500 Subject: polish release notes --- docs/release-notes.md | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) (limited to 'docs') diff --git a/docs/release-notes.md b/docs/release-notes.md index 13735e76..fca18bd2 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -4,8 +4,8 @@ ## Upcoming release * For players: - * SMAPI now prevents mods from crashing the game with invalid schedule data. - * SMAPI now prevents load crashes due to invalid building types. + * SMAPI now prevents crashes due to mods adding invalid schedule data. + * SMAPI now prevents crashes due to invalid building types. * Added support for persistent `smapi-internal/config.json` overrides (see info in the file). * Updated minimum game version (1.4 → 1.4.1). * Fixed 'collection was modified' error when returning to title in rare cases. @@ -24,11 +24,13 @@ * For modders: * Added support for loading `.tmx` map files. * Added JSON converter for `Vector2` values, so they work consistently crossplatform. - * Asset propagation for player sprites now affects other players' sprites, and updates recolor maps (e.g. sleeves). * Reworked the order that asset editors/loaders are called between multiple mods to support some framework mods like Content Patcher and Json Assets. Note that the order is undefined and should not be depended on. * Removed invalid-schedule validation which had false positives. - * Fixed `helper.Data.Read/WriteGlobalData` using the `Saves` folder. The installer will move existing folders to the appdata folder. + * Fixed asset propagation not updating other players' sprites. + * Fixed asset propagation for player sprites not updating recolor maps (e.g. sleeves). + * Fixed `helper.Data.Read/WriteGlobalData` using the `Saves` folder instead of the game's appdata folder. The installer will move existing folders automatically. * Fixed dialogue asset changes not correctly propagated until the next day. + * Fixed dialogue asset propagation for marriage dialogue. * Fixed issue where a mod which implemented `IAssetEditor`/`IAssetLoader` on its entry class could then remove itself from the editor/loader list. * For SMAPI/tool developers: @@ -39,7 +41,7 @@ * Dropped API support for the pre-3.0 update-check format. ## 3.1 -Released 05 January 2019 for Stardew Valley 1.4 or later. +Released 05 January 2019 for Stardew Valley 1.4.1 or later. * For players: * Added separate group in 'skipped mods' list for broken dependencies, so it's easier to see what to fix first. -- cgit From d0885831c3698bcb353d76c147500b7ea1dfbc78 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 1 Feb 2020 11:09:21 -0500 Subject: ignore Harmony DLL in mod build package --- docs/technical/mod-package.md | 7 ++++++- src/SMAPI.ModBuildConfig/Framework/ModFileManager.cs | 3 +++ src/SMAPI.ModBuildConfig/package.nuspec | 15 +++------------ src/SMAPI.Mods.ConsoleCommands/manifest.json | 4 ++-- 4 files changed, 14 insertions(+), 15 deletions(-) (limited to 'docs') diff --git a/docs/technical/mod-package.md b/docs/technical/mod-package.md index 5b971f96..a54d3011 100644 --- a/docs/technical/mod-package.md +++ b/docs/technical/mod-package.md @@ -287,7 +287,11 @@ That will create a `Pathoschild.Stardew.ModBuildConfig-.nupkg` file in which can be uploaded to NuGet or referenced directly. ## Release notes -### Upcoming release +### 3.1 +* Added support for semantic versioning 2.0. +* `0Harmony.dll` is now ignored if the mod references it directly (it's bundled with SMAPI). + +### 3.0 * Updated for SMAPI 3.0 and Stardew Valley 1.4. * Added automatic support for `assets` folders. * Added `$(GameExecutableName)` MSBuild variable. @@ -298,6 +302,7 @@ which can be uploaded to NuGet or referenced directly. * Fixed `Newtonsoft.Json.pdb` included in release zips when Json.NET is referenced directly. * Fixed `` not working for `i18n` files. * Dropped support for older versions of SMAPI and Visual Studio. +* Migrated package icon to NuGet's new format. ### 2.2 * Added support for SMAPI 2.8+ (still compatible with earlier versions). diff --git a/src/SMAPI.ModBuildConfig/Framework/ModFileManager.cs b/src/SMAPI.ModBuildConfig/Framework/ModFileManager.cs index a852f133..f0363a3e 100644 --- a/src/SMAPI.ModBuildConfig/Framework/ModFileManager.cs +++ b/src/SMAPI.ModBuildConfig/Framework/ModFileManager.cs @@ -156,6 +156,9 @@ namespace StardewModdingAPI.ModBuildConfig.Framework // release zips this.EqualsInvariant(file.Extension, ".zip") + // Harmony (bundled into SMAPI) + || this.EqualsInvariant(file.Name, "0Harmony.dll") + // Json.NET (bundled into SMAPI) || this.EqualsInvariant(file.Name, "Newtonsoft.Json.dll") || this.EqualsInvariant(file.Name, "Newtonsoft.Json.pdb") diff --git a/src/SMAPI.ModBuildConfig/package.nuspec b/src/SMAPI.ModBuildConfig/package.nuspec index 846f438d..c5297b46 100644 --- a/src/SMAPI.ModBuildConfig/package.nuspec +++ b/src/SMAPI.ModBuildConfig/package.nuspec @@ -14,18 +14,9 @@ https://raw.githubusercontent.com/Pathoschild/SMAPI/develop/src/SMAPI.ModBuildConfig/assets/nuget-icon.png Automates the build configuration for crossplatform Stardew Valley SMAPI mods. For SMAPI 3.0 or later. - 3.0.0: - - Updated for SMAPI 3.0 and Stardew Valley 1.4. - - Added automatic support for 'assets' folders. - - Added $(GameExecutableName) MSBuild variable. - - Added support for projects using the simplified .csproj format. - - Added option to disable game debugging config. - - Added .pdb files to builds by default (to enable line numbers in error stack traces). - - Added optional Harmony reference. - - Fixed Newtonsoft.Json.pdb included in release zips when Json.NET is referenced directly. - - Fixed <IgnoreModFilePatterns> not working for i18n files. - - Dropped support for older versions of SMAPI and Visual Studio. - - Migrated package icon to NuGet's new format. + 3.1.0: + - Added support for semantic versioning 2.0. + - 0Harmony.dll is now ignored if the mod references it directly (it's bundled with SMAPI). diff --git a/src/SMAPI.Mods.ConsoleCommands/manifest.json b/src/SMAPI.Mods.ConsoleCommands/manifest.json index 1e12e13e..0d0e4901 100644 --- a/src/SMAPI.Mods.ConsoleCommands/manifest.json +++ b/src/SMAPI.Mods.ConsoleCommands/manifest.json @@ -1,9 +1,9 @@ { "Name": "Console Commands", "Author": "SMAPI", - "Version": "3.1.0", + "Version": "3.2.0", "Description": "Adds SMAPI console commands that let you manipulate the game.", "UniqueID": "SMAPI.ConsoleCommands", "EntryDll": "ConsoleCommands.dll", - "MinimumApiVersion": "3.1.0" + "MinimumApiVersion": "3.2.0" } -- cgit From aeb72586fdd94219cb9ae11cfd9f162765a5bc51 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 1 Feb 2020 11:11:44 -0500 Subject: prepare for release --- build/common.targets | 2 +- docs/release-notes.md | 20 +++++++++++--------- docs/technical/mod-package.md | 2 +- src/SMAPI.ModBuildConfig/package.nuspec | 2 +- src/SMAPI.Mods.SaveBackup/manifest.json | 4 ++-- src/SMAPI/Constants.cs | 2 +- 6 files changed, 17 insertions(+), 15 deletions(-) (limited to 'docs') diff --git a/build/common.targets b/build/common.targets index cfdcccca..8b0d1301 100644 --- a/build/common.targets +++ b/build/common.targets @@ -4,7 +4,7 @@ - 3.1.0 + 3.2.0 SMAPI $(AssemblySearchPaths);{GAC} diff --git a/docs/release-notes.md b/docs/release-notes.md index fca18bd2..f1981218 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -1,17 +1,18 @@ ← [README](README.md) # Release notes -## Upcoming release +## 3.2 +Released 01 February 2020 for Stardew Valley 1.4.1 or later. * For players: - * SMAPI now prevents crashes due to mods adding invalid schedule data. + * SMAPI now prevents crashes due to invalid schedule data. * SMAPI now prevents crashes due to invalid building types. * Added support for persistent `smapi-internal/config.json` overrides (see info in the file). * Updated minimum game version (1.4 → 1.4.1). * Fixed 'collection was modified' error when returning to title in rare cases. - * Fixed update-check error if a mod's Chucklefish page has no version. - * Fixed SMAPI beta versions not showing update alert on next launch (thanks to danvolchek!). + * Fixed error when update-checking a mod with a Chucklefish page that has no version. * Fixed rare error when building/demolishing buildings. + * Fixed SMAPI beta versions not showing update alert on next launch (thanks to danvolchek!). * For the Console Commands mod: * Added `performance` command to track mod performance metrics. This is an advanced experimental feature. (Thanks to Drachenkätzchen!) @@ -22,20 +23,21 @@ * Fixed Android issue where game files were backed up. * For modders: - * Added support for loading `.tmx` map files. - * Added JSON converter for `Vector2` values, so they work consistently crossplatform. + * Added support for `.tmx` map files. + * Added special handling for `Vector2` values in `.json` files, so they work consistently crossplatform. * Reworked the order that asset editors/loaders are called between multiple mods to support some framework mods like Content Patcher and Json Assets. Note that the order is undefined and should not be depended on. - * Removed invalid-schedule validation which had false positives. + * Fixed incorrect warning about mods adding invalid schedules in some cases. The validation was unreliable, and has been removed. * Fixed asset propagation not updating other players' sprites. * Fixed asset propagation for player sprites not updating recolor maps (e.g. sleeves). - * Fixed `helper.Data.Read/WriteGlobalData` using the `Saves` folder instead of the game's appdata folder. The installer will move existing folders automatically. + * Fixed asset propagation for marriage dialogue. * Fixed dialogue asset changes not correctly propagated until the next day. - * Fixed dialogue asset propagation for marriage dialogue. + * Fixed `helper.Data.Read`/`WriteGlobalData` using the `Saves` folder instead of the game's appdata folder. The installer will move existing folders automatically. * Fixed issue where a mod which implemented `IAssetEditor`/`IAssetLoader` on its entry class could then remove itself from the editor/loader list. * For SMAPI/tool developers: * Added internal performance monitoring (thanks to Drachenkätzchen!). This is disabled by default in the current version, but can be enabled using the `performance` console command. * Added internal support for four-part versions to support SMAPI on Android. + * Rewrote `SemanticVersion` parsing. * Updated links for the new r/SMAPI subreddit. * The `/mods` web API endpoint now includes version mappings from the wiki. * Dropped API support for the pre-3.0 update-check format. diff --git a/docs/technical/mod-package.md b/docs/technical/mod-package.md index a54d3011..e771d7a9 100644 --- a/docs/technical/mod-package.md +++ b/docs/technical/mod-package.md @@ -289,7 +289,7 @@ which can be uploaded to NuGet or referenced directly. ## Release notes ### 3.1 * Added support for semantic versioning 2.0. -* `0Harmony.dll` is now ignored if the mod references it directly (it's bundled with SMAPI). +* `0Harmony.dll` is now ignored if the mod references Harmony directly (it's bundled with SMAPI). ### 3.0 * Updated for SMAPI 3.0 and Stardew Valley 1.4. diff --git a/src/SMAPI.ModBuildConfig/package.nuspec b/src/SMAPI.ModBuildConfig/package.nuspec index c5297b46..afb03cec 100644 --- a/src/SMAPI.ModBuildConfig/package.nuspec +++ b/src/SMAPI.ModBuildConfig/package.nuspec @@ -2,7 +2,7 @@ Pathoschild.Stardew.ModBuildConfig - 3.0.0 + 3.1.0 Build package for SMAPI mods Pathoschild Pathoschild diff --git a/src/SMAPI.Mods.SaveBackup/manifest.json b/src/SMAPI.Mods.SaveBackup/manifest.json index 55af8f35..74256013 100644 --- a/src/SMAPI.Mods.SaveBackup/manifest.json +++ b/src/SMAPI.Mods.SaveBackup/manifest.json @@ -1,9 +1,9 @@ { "Name": "Save Backup", "Author": "SMAPI", - "Version": "3.1.0", + "Version": "3.2.0", "Description": "Automatically backs up all your saves once per day into its folder.", "UniqueID": "SMAPI.SaveBackup", "EntryDll": "SaveBackup.dll", - "MinimumApiVersion": "3.1.0" + "MinimumApiVersion": "3.2.0" } diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs index 8afe4b52..201d3166 100644 --- a/src/SMAPI/Constants.cs +++ b/src/SMAPI/Constants.cs @@ -20,7 +20,7 @@ namespace StardewModdingAPI ** Public ****/ /// SMAPI's current semantic version. - public static ISemanticVersion ApiVersion { get; } = new Toolkit.SemanticVersion("3.1.0"); + public static ISemanticVersion ApiVersion { get; } = new Toolkit.SemanticVersion("3.2.0"); /// The minimum supported version of Stardew Valley. public static ISemanticVersion MinimumGameVersion { get; } = new GameVersion("1.4.1"); -- cgit