From 5622e3b319cf7a312537f2a555fee9e67628b1cd Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 24 Aug 2017 20:44:44 -0400 Subject: fix map tilesheet load not handling seasonal variations (#352) --- .../Framework/ModHelpers/ContentHelper.cs | 92 +++++++++++++++++----- 1 file changed, 72 insertions(+), 20 deletions(-) (limited to 'src') diff --git a/src/StardewModdingAPI/Framework/ModHelpers/ContentHelper.cs b/src/StardewModdingAPI/Framework/ModHelpers/ContentHelper.cs index ffa78ff6..21201970 100644 --- a/src/StardewModdingAPI/Framework/ModHelpers/ContentHelper.cs +++ b/src/StardewModdingAPI/Framework/ModHelpers/ContentHelper.cs @@ -216,47 +216,99 @@ namespace StardewModdingAPI.Framework.ModHelpers /// The map tilesheets could not be loaded. private void FixLocalMapTilesheets(Map map, string mapKey) { + // check map info if (!map.TileSheets.Any()) return; - string relativeMapFolder = Path.GetDirectoryName(mapKey) ?? ""; // folder path containing the map, relative to the mod folder + + // fix tilesheets foreach (TileSheet tilesheet in map.TileSheets) { - // check for tilesheet relative to map + string imageSource = tilesheet.ImageSource; + + // get seasonal name (if applicable) + string seasonalImageSource = null; + if(Game1.currentSeason != null && Game1.currentSeason != "spring") + { + string filename = Path.GetFileName(imageSource); + string dirPath = imageSource.Substring(0, imageSource.LastIndexOf(filename)); + if (filename.StartsWith("spring_")) + seasonalImageSource = dirPath + Game1.currentSeason + "_" + filename.Substring("spring_".Length); + } + + // load best match + try { - string localKey = Path.Combine(relativeMapFolder, tilesheet.ImageSource); - FileInfo localFile = this.GetModFile(localKey); - if (localFile.Exists) + string key = + this.TryLoadTilesheetImageSource(relativeMapFolder, seasonalImageSource) + ?? this.TryLoadTilesheetImageSource(relativeMapFolder, imageSource); + if (key != null) { - try - { - this.Load(localKey); - } - catch (Exception ex) - { - throw new ContentLoadException($"The local '{tilesheet.ImageSource}' tilesheet couldn't be loaded.", ex); - } - tilesheet.ImageSource = this.GetActualAssetKey(localKey); + tilesheet.ImageSource = key; continue; } } + catch (Exception ex) + { + throw new ContentLoadException($"The '{imageSource}' tilesheet couldn't be loaded relative to either map file or the game's content folder.", ex); + } + + // none found + throw new ContentLoadException($"The '{imageSource}' tilesheet couldn't be loaded relative to either map file or the game's content folder."); + } + } + + /// Load a tilesheet image source if the file exists. + /// The folder path containing the map, relative to the mod folder. + /// The tilesheet image source to load. + /// Returns the loaded asset key (if it was loaded successfully). + private string TryLoadTilesheetImageSource(string relativeMapFolder, string imageSource) + { + if (imageSource == null) + return null; + + // check for tilesheet relative to map + { + string localKey = Path.Combine(relativeMapFolder, imageSource); + FileInfo localFile = this.GetModFile(localKey); + if (localFile.Exists) + { + try + { + this.Load(localKey); + } + catch (Exception ex) + { + throw new ContentLoadException($"The local '{imageSource}' tilesheet couldn't be loaded.", ex); + } + + return this.GetActualAssetKey(localKey); + } + } + + // fallback to game content + { + string contentKey = imageSource.EndsWith(".png") + ? imageSource.Substring(0, imageSource.Length - 4) + : imageSource; - // fallback to game content + FileInfo file = new FileInfo(Path.Combine(this.ContentManager.FullRootDirectory, contentKey + ".xnb")); + if (file.Exists) { - string contentKey = tilesheet.ImageSource; - if (contentKey.EndsWith(".png")) - contentKey = contentKey.Substring(0, contentKey.Length - 4); try { this.ContentManager.Load(contentKey); } catch (Exception ex) { - throw new ContentLoadException($"The '{tilesheet.ImageSource}' tilesheet couldn't be loaded relative to either map file or the game's content folder.", ex); + throw new ContentLoadException($"The '{imageSource}' tilesheet couldn't be loaded relative to either map file or the game's content folder.", ex); } - tilesheet.ImageSource = contentKey; + return contentKey; } } + + // not found + return null; } /// Assert that the given key has a valid format. -- cgit From f446a4391aef4e239a53736c42a2652bb2c6fead Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 24 Aug 2017 20:48:06 -0400 Subject: fix game's main temporary content manager not being intercepted (#352) --- src/StardewModdingAPI/Framework/SGame.cs | 1 + 1 file changed, 1 insertion(+) (limited to 'src') diff --git a/src/StardewModdingAPI/Framework/SGame.cs b/src/StardewModdingAPI/Framework/SGame.cs index 997e0c8c..2bfbc06a 100644 --- a/src/StardewModdingAPI/Framework/SGame.cs +++ b/src/StardewModdingAPI/Framework/SGame.cs @@ -203,6 +203,7 @@ namespace StardewModdingAPI.Framework this.SContentManager = new SContentManager(this.Content.ServiceProvider, this.Content.RootDirectory, Thread.CurrentThread.CurrentUICulture, null, this.Monitor); this.Content = this.SContentManager; Game1.content = this.SContentManager; + reflection.GetPrivateField(typeof(Game1), "_temporaryContent").SetValue(null); // regenerate value with new content manager } /**** -- cgit From 5171829ecc8acd8dc0e3292bd3c2c7893a148c8f Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 24 Aug 2017 21:48:56 -0400 Subject: restructure content manager to better handle asset disposal (#352) --- .../Framework/ContentManagerShim.cs | 50 ++++++++++++++++ src/StardewModdingAPI/Framework/SContentManager.cs | 69 ++++++++++++++++------ src/StardewModdingAPI/Framework/SGame.cs | 14 ++--- src/StardewModdingAPI/Program.cs | 2 +- src/StardewModdingAPI/StardewModdingAPI.csproj | 1 + 5 files changed, 110 insertions(+), 26 deletions(-) create mode 100644 src/StardewModdingAPI/Framework/ContentManagerShim.cs (limited to 'src') diff --git a/src/StardewModdingAPI/Framework/ContentManagerShim.cs b/src/StardewModdingAPI/Framework/ContentManagerShim.cs new file mode 100644 index 00000000..d46f23a3 --- /dev/null +++ b/src/StardewModdingAPI/Framework/ContentManagerShim.cs @@ -0,0 +1,50 @@ +using StardewValley; + +namespace StardewModdingAPI.Framework +{ + /// A minimal content manager which defers to SMAPI's main content manager. + internal class ContentManagerShim : LocalizedContentManager + { + /********* + ** Properties + *********/ + /// SMAPI's underlying content manager. + private readonly SContentManager ContentManager; + + + /********* + ** Accessors + *********/ + /// The content manager's name for logs (if any). + public string Name { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// SMAPI's underlying content manager. + /// The content manager's name for logs (if any). + public ContentManagerShim(SContentManager contentManager, string name) + : base(contentManager.ServiceProvider, contentManager.RootDirectory, contentManager.CurrentCulture, contentManager.LanguageCodeOverride) + { + this.ContentManager = contentManager; + this.Name = name; + } + + /// Load an asset that has been processed by the content pipeline. + /// The type of asset to load. + /// The asset path relative to the loader root directory, not including the .xnb extension. + public override T Load(string assetName) + { + return this.ContentManager.LoadFor(assetName, this); + } + + /// Dispose held resources. + /// Whether the content manager is disposing (rather than finalising). + protected override void Dispose(bool disposing) + { + this.ContentManager.DisposeFor(this); + } + } +} diff --git a/src/StardewModdingAPI/Framework/SContentManager.cs b/src/StardewModdingAPI/Framework/SContentManager.cs index 25775291..4c4eee58 100644 --- a/src/StardewModdingAPI/Framework/SContentManager.cs +++ b/src/StardewModdingAPI/Framework/SContentManager.cs @@ -48,6 +48,9 @@ namespace StardewModdingAPI.Framework /// The assets currently being intercepted by instances. This is used to prevent infinite loops when a loader loads a new asset. private readonly ContextHash AssetsBeingLoaded = new ContextHash(); + /// A lookup of the content managers which loaded each asset. + private readonly IDictionary> AssetLoaders = new Dictionary>(); + /********* ** Accessors @@ -98,7 +101,6 @@ namespace StardewModdingAPI.Framework // get asset data this.CoreAssets = new CoreAssets(this.NormaliseAssetName); this.KeyLocales = this.GetKeyLocales(reflection); - } /// Normalise path separators in a file path. For asset keys, see instead. @@ -134,12 +136,24 @@ namespace StardewModdingAPI.Framework /// The type of asset to load. /// The asset path relative to the loader root directory, not including the .xnb extension. public override T Load(string assetName) + { + return this.LoadFor(assetName, this); + } + + /// Load an asset that has been processed by the content pipeline. + /// The type of asset to load. + /// The asset path relative to the loader root directory, not including the .xnb extension. + /// The content manager instance for which to load the asset. + public T LoadFor(string assetName, ContentManager instance) { assetName = this.NormaliseAssetName(assetName); // skip if already loaded if (this.IsNormalisedKeyLoaded(assetName)) + { + this.TrackAssetLoader(assetName, instance); return base.Load(assetName); + } // load asset T data; @@ -162,6 +176,7 @@ namespace StardewModdingAPI.Framework // update cache & return data this.Cache[assetName] = data; + this.TrackAssetLoader(assetName, instance); return data; } @@ -172,8 +187,8 @@ namespace StardewModdingAPI.Framework public void Inject(string assetName, T value) { assetName = this.NormaliseAssetName(assetName); - this.Cache[assetName] = value; + this.TrackAssetLoader(assetName, this); } /// Get the current content locale. @@ -229,8 +244,9 @@ namespace StardewModdingAPI.Framework /// Purge matched assets from the cache. /// Matches the asset keys to invalidate. + /// Whether to dispose invalidated assets. This should only be true when they're being invalidated as part of a dispose, to avoid crashing the game. /// Returns whether any cache entries were invalidated. - public bool InvalidateCache(Func predicate) + public bool InvalidateCache(Func predicate, bool dispose = true) { // find matching asset keys HashSet purgeCacheKeys = new HashSet(StringComparer.InvariantCultureIgnoreCase); @@ -246,9 +262,14 @@ namespace StardewModdingAPI.Framework } } - // purge from cache + // purge assets foreach (string key in purgeCacheKeys) + { + if (dispose && this.Cache[key] is IDisposable disposable) + disposable.Dispose(); this.Cache.Remove(key); + this.AssetLoaders.Remove(key); + } // reload core game assets int reloaded = 0; @@ -268,6 +289,19 @@ namespace StardewModdingAPI.Framework return false; } + /// Dispose assets for the given content manager shim. + /// The content manager whose assets to dispose. + internal void DisposeFor(ContentManagerShim shim) + { + this.Monitor.Log($"Content manager '{shim.Name}' disposed, disposing assets that aren't needed by any other asset loader.", LogLevel.Trace); + HashSet keys = new HashSet( + from entry in this.AssetLoaders + where entry.Value.Count == 1 && entry.Value.First() == shim + select entry.Key + ); + this.InvalidateCache((key, type) => keys.Contains(key)); + } + /********* ** Private methods @@ -280,6 +314,16 @@ namespace StardewModdingAPI.Framework || this.Cache.ContainsKey($"{normalisedAssetName}.{this.GetKeyLocale.Invoke()}"); // translated asset } + /// Track that a content manager loaded an asset. + /// The asset key that was loaded. + /// The content manager that loaded the asset. + private void TrackAssetLoader(string key, ContentManager manager) + { + if (!this.AssetLoaders.TryGetValue(key, out HashSet hash)) + hash = this.AssetLoaders[key] = new HashSet(); + hash.Add(manager); + } + /// Get the locale codes (like ja-JP) used in asset keys. /// Simplifies access to private game code. private IDictionary GetKeyLocales(Reflector reflection) @@ -463,23 +507,12 @@ namespace StardewModdingAPI.Framework } } - /// Dispose all game resources. + /// Dispose held resources. /// Whether the content manager is disposing (rather than finalising). protected override void Dispose(bool disposing) { - if (!disposing) - return; - - // Clear cache & reload all assets. While that may seem perverse during disposal, it's - // necessary due to limitations in the way SMAPI currently intercepts content assets. - // - // The game uses multiple content managers while SMAPI needs one and only one. The game - // only disposes some of its content managers when returning to title, which means SMAPI - // can't know which assets are meant to be disposed. Here we remove current assets from - // the cache, but don't dispose them to avoid crashing any code that still references - // them. The garbage collector will eventually clean up any unused assets. - this.Monitor.Log("Content manager disposed, resetting cache.", LogLevel.Trace); - this.InvalidateCache((key, type) => true); + this.Monitor.Log("Disposing SMAPI's main content manager. It will no longer be usable after this point.", LogLevel.Trace); + base.Dispose(disposing); } } } diff --git a/src/StardewModdingAPI/Framework/SGame.cs b/src/StardewModdingAPI/Framework/SGame.cs index 2bfbc06a..76c106d7 100644 --- a/src/StardewModdingAPI/Framework/SGame.cs +++ b/src/StardewModdingAPI/Framework/SGame.cs @@ -38,9 +38,6 @@ namespace StardewModdingAPI.Framework /// Encapsulates monitoring and logging. private readonly IMonitor Monitor; - /// SMAPI's content manager. - private readonly SContentManager SContentManager; - /// The maximum number of consecutive attempts SMAPI should make to recover from a draw error. private readonly Countdown DrawCrashTimer = new Countdown(60); // 60 ticks = roughly one second @@ -177,6 +174,9 @@ namespace StardewModdingAPI.Framework /********* ** Accessors *********/ + /// SMAPI's content manager. + public SContentManager SContentManager { get; } + /// Whether SMAPI should log more information about the game context. public bool VerboseLogging { get; set; } @@ -201,9 +201,9 @@ namespace StardewModdingAPI.Framework // override content manager this.Monitor?.Log("Overriding content manager...", LogLevel.Trace); this.SContentManager = new SContentManager(this.Content.ServiceProvider, this.Content.RootDirectory, Thread.CurrentThread.CurrentUICulture, null, this.Monitor); - this.Content = this.SContentManager; - Game1.content = this.SContentManager; - reflection.GetPrivateField(typeof(Game1), "_temporaryContent").SetValue(null); // regenerate value with new content manager + this.Content = new ContentManagerShim(this.SContentManager, "SGame.Content"); + Game1.content = new ContentManagerShim(this.SContentManager, "Game1.content"); + reflection.GetPrivateField(typeof(Game1), "_temporaryContent").SetValue(new ContentManagerShim(this.SContentManager, "Game1._temporaryContent")); // regenerate value with new content manager } /**** @@ -226,7 +226,7 @@ namespace StardewModdingAPI.Framework throw new InvalidOperationException("SMAPI uses a single content manager internally. You can't get a new content manager with a different service provider."); if (rootDirectory != this.Content.RootDirectory) throw new InvalidOperationException($"SMAPI uses a single content manager internally. You can't get a new content manager with a different root directory (current is {this.Content.RootDirectory}, requested {rootDirectory})."); - return this.SContentManager; + return new ContentManagerShim(this.SContentManager, "(generated instance)"); } /// The method called when the game is updating its state. This happens roughly 60 times per second. diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index 108e9273..13d52f12 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -53,7 +53,7 @@ namespace StardewModdingAPI private SGame GameInstance; /// The underlying content manager. - private SContentManager ContentManager => (SContentManager)this.GameInstance.Content; + private SContentManager ContentManager => this.GameInstance.SContentManager; /// The SMAPI configuration settings. /// This is initialised after the game starts. diff --git a/src/StardewModdingAPI/StardewModdingAPI.csproj b/src/StardewModdingAPI/StardewModdingAPI.csproj index 8c7279a1..8daf21b7 100644 --- a/src/StardewModdingAPI/StardewModdingAPI.csproj +++ b/src/StardewModdingAPI/StardewModdingAPI.csproj @@ -91,6 +91,7 @@ Properties\GlobalAssemblyInfo.cs + -- cgit From 7167cd2253ae9320e34eff5f2c6d6dd9ad0d0eef Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 24 Aug 2017 22:17:42 -0400 Subject: simplify & fix asset disposal (#352) --- src/StardewModdingAPI/Framework/SContentManager.cs | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) (limited to 'src') diff --git a/src/StardewModdingAPI/Framework/SContentManager.cs b/src/StardewModdingAPI/Framework/SContentManager.cs index 4c4eee58..9553e79f 100644 --- a/src/StardewModdingAPI/Framework/SContentManager.cs +++ b/src/StardewModdingAPI/Framework/SContentManager.cs @@ -246,7 +246,7 @@ namespace StardewModdingAPI.Framework /// Matches the asset keys to invalidate. /// Whether to dispose invalidated assets. This should only be true when they're being invalidated as part of a dispose, to avoid crashing the game. /// Returns whether any cache entries were invalidated. - public bool InvalidateCache(Func predicate, bool dispose = true) + public bool InvalidateCache(Func predicate, bool dispose = false) { // find matching asset keys HashSet purgeCacheKeys = new HashSet(StringComparer.InvariantCultureIgnoreCase); @@ -294,12 +294,10 @@ namespace StardewModdingAPI.Framework internal void DisposeFor(ContentManagerShim shim) { this.Monitor.Log($"Content manager '{shim.Name}' disposed, disposing assets that aren't needed by any other asset loader.", LogLevel.Trace); - HashSet keys = new HashSet( - from entry in this.AssetLoaders - where entry.Value.Count == 1 && entry.Value.First() == shim - select entry.Key - ); - this.InvalidateCache((key, type) => keys.Contains(key)); + + foreach (var entry in this.AssetLoaders) + entry.Value.Remove(shim); + this.InvalidateCache((key, type) => !this.AssetLoaders[key].Any(), dispose: true); } -- cgit From da11ea66dbe3dd033f4081baea5b439e2c11d0e8 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 1 Sep 2017 14:51:12 -0400 Subject: add SButton extension to get InputButton equivalent --- src/StardewModdingAPI/Utilities/SButton.cs | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/StardewModdingAPI/Utilities/SButton.cs b/src/StardewModdingAPI/Utilities/SButton.cs index c4833b0b..33058a64 100644 --- a/src/StardewModdingAPI/Utilities/SButton.cs +++ b/src/StardewModdingAPI/Utilities/SButton.cs @@ -1,5 +1,6 @@ -using System; +using System; using Microsoft.Xna.Framework.Input; +using StardewValley; namespace StardewModdingAPI.Utilities { @@ -655,5 +656,30 @@ namespace StardewModdingAPI.Utilities button = 0; return false; } + + /// Get the equivalent for the given button. + /// The button to convert. + /// The Stardew Valley input button equivalent. + /// Returns whether the value was converted successfully. + public static bool TryGetStardewInput(this SButton input, out InputButton button) + { + // keyboard + if (input.TryGetKeyboard(out Keys key)) + { + button = new InputButton(key); + return true; + } + + // mouse + if (input == SButton.MouseLeft || input == SButton.MouseRight) + { + button = new InputButton(mouseLeft: input == SButton.MouseLeft); + return true; + } + + // not valid + button = default(InputButton); + return false; + } } } -- cgit From dac21226d275fca4ce4880ab9e15104f7987c133 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 1 Sep 2017 21:05:08 -0400 Subject: fix IAssetLoader instances not able to load a map tilesheet if it doesn't also exist in the content folder (#352) --- .../Framework/ModHelpers/ContentHelper.cs | 39 ++++++++++++++++------ 1 file changed, 28 insertions(+), 11 deletions(-) (limited to 'src') diff --git a/src/StardewModdingAPI/Framework/ModHelpers/ContentHelper.cs b/src/StardewModdingAPI/Framework/ModHelpers/ContentHelper.cs index 21201970..d24124e0 100644 --- a/src/StardewModdingAPI/Framework/ModHelpers/ContentHelper.cs +++ b/src/StardewModdingAPI/Framework/ModHelpers/ContentHelper.cs @@ -228,7 +228,7 @@ namespace StardewModdingAPI.Framework.ModHelpers // get seasonal name (if applicable) string seasonalImageSource = null; - if(Game1.currentSeason != null && Game1.currentSeason != "spring") + if (Game1.currentSeason != null && Game1.currentSeason != "spring") { string filename = Path.GetFileName(imageSource); string dirPath = imageSource.Substring(0, imageSource.LastIndexOf(filename)); @@ -292,19 +292,23 @@ namespace StardewModdingAPI.Framework.ModHelpers ? imageSource.Substring(0, imageSource.Length - 4) : imageSource; - FileInfo file = new FileInfo(Path.Combine(this.ContentManager.FullRootDirectory, contentKey + ".xnb")); - if (file.Exists) + try { - try - { - this.ContentManager.Load(contentKey); - } - catch (Exception ex) - { - throw new ContentLoadException($"The '{imageSource}' tilesheet couldn't be loaded relative to either map file or the game's content folder.", ex); - } + this.Load(contentKey, ContentSource.GameContent); return contentKey; } + catch + { + // ignore file-not-found errors + // TODO: while it's useful to suppress a asset-not-found error here to avoid + // confusion, this is a pretty naive approach. Even if the file doesn't exist, + // the file may have been loaded through an IAssetLoader which failed. So even + // if the content file doesn't exist, that doesn't mean the error here is a + // content-not-found error. Unfortunately XNA doesn't provide a good way to + // detect the error type. + if (this.GetContentFolderFile(contentKey).Exists) + throw; + } } // not found @@ -342,6 +346,19 @@ namespace StardewModdingAPI.Framework.ModHelpers return file; } + /// Get a file from the game's content folder. + /// The asset key. + private FileInfo GetContentFolderFile(string key) + { + // get file path + string path = Path.Combine(this.ContentManager.FullRootDirectory, key); + if (!path.EndsWith(".xnb")) + path += ".xnb"; + + // get file + return new FileInfo(path); + } + /// Get the asset path which loads a mod folder through a content manager. /// The file path relative to the mod's folder. /// The absolute file path. -- cgit From 3e820b82bccf7cab390bc43d9156fdeb4ab08cd9 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 2 Sep 2017 23:58:43 -0400 Subject: account for game loading tilesheets from either Content or Content\Maps (#352) --- .../Framework/ModHelpers/ContentHelper.cs | 58 ++++++++++++++-------- 1 file changed, 37 insertions(+), 21 deletions(-) (limited to 'src') diff --git a/src/StardewModdingAPI/Framework/ModHelpers/ContentHelper.cs b/src/StardewModdingAPI/Framework/ModHelpers/ContentHelper.cs index d24124e0..a3beaedf 100644 --- a/src/StardewModdingAPI/Framework/ModHelpers/ContentHelper.cs +++ b/src/StardewModdingAPI/Framework/ModHelpers/ContentHelper.cs @@ -214,6 +214,18 @@ namespace StardewModdingAPI.Framework.ModHelpers /// The map whose tilesheets to fix. /// The map asset key within the mod folder. /// The map tilesheets could not be loaded. + /// + /// The game's logic for tilesheets in is a bit specialised. It boils down to this: + /// * If the location is indoors or the desert, or the image source contains 'path' or 'object', it's loaded as-is relative to the Content folder. + /// * Else it's loaded from Content\Maps with a seasonal prefix. + /// + /// That logic doesn't work well in our case, mainly because we have no location metadata at this point. + /// Instead we use a more heuristic approach: check relative to the map file first, then relative to + /// Content\Maps, then Content. If the image source filename contains a seasonal prefix, we try + /// for a seasonal variation and then an exact match. + /// + /// While that doesn't exactly match the game logic, it's close enough that it's unlikely to make a difference. + /// private void FixLocalMapTilesheets(Map map, string mapKey) { // check map info @@ -262,12 +274,13 @@ namespace StardewModdingAPI.Framework.ModHelpers /// The folder path containing the map, relative to the mod folder. /// The tilesheet image source to load. /// Returns the loaded asset key (if it was loaded successfully). + /// See remarks on . private string TryLoadTilesheetImageSource(string relativeMapFolder, string imageSource) { if (imageSource == null) return null; - // check for tilesheet relative to map + // check relative to map file { string localKey = Path.Combine(relativeMapFolder, imageSource); FileInfo localFile = this.GetModFile(localKey); @@ -286,28 +299,31 @@ namespace StardewModdingAPI.Framework.ModHelpers } } - // fallback to game content + // check relative to content folder { - string contentKey = imageSource.EndsWith(".png") - ? imageSource.Substring(0, imageSource.Length - 4) - : imageSource; - - try + foreach (string candidateKey in new[] { imageSource, $@"Maps\{imageSource}" }) { - this.Load(contentKey, ContentSource.GameContent); - return contentKey; - } - catch - { - // ignore file-not-found errors - // TODO: while it's useful to suppress a asset-not-found error here to avoid - // confusion, this is a pretty naive approach. Even if the file doesn't exist, - // the file may have been loaded through an IAssetLoader which failed. So even - // if the content file doesn't exist, that doesn't mean the error here is a - // content-not-found error. Unfortunately XNA doesn't provide a good way to - // detect the error type. - if (this.GetContentFolderFile(contentKey).Exists) - throw; + string contentKey = candidateKey.EndsWith(".png") + ? candidateKey.Substring(0, imageSource.Length - 4) + : candidateKey; + + try + { + this.Load(contentKey, ContentSource.GameContent); + return contentKey; + } + catch + { + // ignore file-not-found errors + // TODO: while it's useful to suppress a asset-not-found error here to avoid + // confusion, this is a pretty naive approach. Even if the file doesn't exist, + // the file may have been loaded through an IAssetLoader which failed. So even + // if the content file doesn't exist, that doesn't mean the error here is a + // content-not-found error. Unfortunately XNA doesn't provide a good way to + // detect the error type. + if (this.GetContentFolderFile(contentKey).Exists) + throw; + } } } -- cgit From b86d9f7c0ebdd3bc1200836137f92eaa3c9910ec Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 3 Sep 2017 00:00:39 -0400 Subject: handle maps referencing a non-spring seasonal variation (#352) --- .../Framework/ModHelpers/ContentHelper.cs | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) (limited to 'src') diff --git a/src/StardewModdingAPI/Framework/ModHelpers/ContentHelper.cs b/src/StardewModdingAPI/Framework/ModHelpers/ContentHelper.cs index a3beaedf..9f236823 100644 --- a/src/StardewModdingAPI/Framework/ModHelpers/ContentHelper.cs +++ b/src/StardewModdingAPI/Framework/ModHelpers/ContentHelper.cs @@ -240,12 +240,19 @@ namespace StardewModdingAPI.Framework.ModHelpers // get seasonal name (if applicable) string seasonalImageSource = null; - if (Game1.currentSeason != null && Game1.currentSeason != "spring") + if (Game1.currentSeason != null) { string filename = Path.GetFileName(imageSource); - string dirPath = imageSource.Substring(0, imageSource.LastIndexOf(filename)); - if (filename.StartsWith("spring_")) - seasonalImageSource = dirPath + Game1.currentSeason + "_" + filename.Substring("spring_".Length); + bool hasSeasonalPrefix = + filename.StartsWith("spring_", StringComparison.CurrentCultureIgnoreCase) + || filename.StartsWith("summer_", StringComparison.CurrentCultureIgnoreCase) + || filename.StartsWith("fall_", StringComparison.CurrentCultureIgnoreCase) + || filename.StartsWith("winter_", StringComparison.CurrentCultureIgnoreCase); + if (hasSeasonalPrefix && !filename.StartsWith(Game1.currentSeason + "_")) + { + string dirPath = imageSource.Substring(0, imageSource.LastIndexOf(filename, StringComparison.CurrentCultureIgnoreCase)); + seasonalImageSource = $"{dirPath}{Game1.currentSeason}_{filename.Substring(filename.IndexOf("_", StringComparison.CurrentCultureIgnoreCase) + 1)}"; + } } // load best match -- cgit From d971514a3d316e1466db51f9709635ce6d4e6b91 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 3 Sep 2017 15:51:38 -0400 Subject: fix '.dll.dll' in logs (#355) --- src/StardewModdingAPI/Framework/ModLoading/AssemblyLoader.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/StardewModdingAPI/Framework/ModLoading/AssemblyLoader.cs b/src/StardewModdingAPI/Framework/ModLoading/AssemblyLoader.cs index e6ec21a6..9c642bef 100644 --- a/src/StardewModdingAPI/Framework/ModLoading/AssemblyLoader.cs +++ b/src/StardewModdingAPI/Framework/ModLoading/AssemblyLoader.cs @@ -91,7 +91,7 @@ namespace StardewModdingAPI.Framework.ModLoading if (changed) { if (!oneAssembly) - this.Monitor.Log($" Loading {assembly.File.Name}.dll (rewritten in memory)...", LogLevel.Trace); + this.Monitor.Log($" Loading {assembly.File.Name} (rewritten in memory)...", LogLevel.Trace); using (MemoryStream outStream = new MemoryStream()) { assembly.Definition.Write(outStream); @@ -102,7 +102,7 @@ namespace StardewModdingAPI.Framework.ModLoading else { if (!oneAssembly) - this.Monitor.Log($" Loading {assembly.File.Name}.dll...", LogLevel.Trace); + this.Monitor.Log($" Loading {assembly.File.Name}...", LogLevel.Trace); lastAssembly = Assembly.UnsafeLoadFrom(assembly.File.FullName); } } -- cgit From b2b3df08bcd2de9c9f9ca98f61a50f4c6d1fd6e5 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 6 Sep 2017 22:04:51 -0400 Subject: rewrite date calculation to handle edge cases --- release-notes.md | 7 ++- .../Utilities/SDateTests.cs | 4 +- src/StardewModdingAPI/Utilities/SDate.cs | 58 ++++++++-------------- 3 files changed, 28 insertions(+), 41 deletions(-) (limited to 'src') diff --git a/release-notes.md b/release-notes.md index 430bd9c8..e5721fd9 100644 --- a/release-notes.md +++ b/release-notes.md @@ -28,11 +28,14 @@ For power users: ## 1.15.4 For players: -* Internal fixes to support Entoarox Framework and the upcoming SMAPI 2.0 release. +* Fixed issues with maps loaded through Entoarox Framework. + +For modders: +* Fixed edge cases in `SDate.AddDays(…)` calculations. ## 1.15.3 For players: -* Fixed mods being marked as duplicate incorrectly in some cases. +* Fixed mods being wrongly marked as duplicate in some cases. ## 1.15.2 For players: diff --git a/src/StardewModdingAPI.Tests/Utilities/SDateTests.cs b/src/StardewModdingAPI.Tests/Utilities/SDateTests.cs index 714756e0..25acbaf3 100644 --- a/src/StardewModdingAPI.Tests/Utilities/SDateTests.cs +++ b/src/StardewModdingAPI.Tests/Utilities/SDateTests.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; @@ -104,6 +104,8 @@ namespace StardewModdingAPI.Tests.Utilities [TestCase("15 summer Y1", -28, ExpectedResult = "15 spring Y1")] // negative season transition [TestCase("15 summer Y2", -28 * 4, ExpectedResult = "15 summer Y1")] // negative year transition [TestCase("01 spring Y3", -(28 * 7 + 17), ExpectedResult = "12 spring Y1")] // negative year transition + [TestCase("06 fall Y2", 50, ExpectedResult = "28 winter Y3")] // test for zero-index errors + [TestCase("06 fall Y2", 51, ExpectedResult = "01 spring Y3")] // test for zero-index errors public string AddDays(string dateStr, int addDays) { return this.GetDate(dateStr).AddDays(addDays).ToString(); diff --git a/src/StardewModdingAPI/Utilities/SDate.cs b/src/StardewModdingAPI/Utilities/SDate.cs index d7631598..5073259d 100644 --- a/src/StardewModdingAPI/Utilities/SDate.cs +++ b/src/StardewModdingAPI/Utilities/SDate.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Linq; using StardewValley; @@ -86,34 +86,27 @@ namespace StardewModdingAPI.Utilities /// The offset would result in an invalid date (like year 0). public SDate AddDays(int offset) { - // simple case - int day = this.Day + offset; - string season = this.Season; - int year = this.Year; + // get new hash code + int hashCode = this.GetHashCode() + offset; + if (hashCode < 1) + throw new ArithmeticException($"Adding {offset} days to {this} would result in a date before 01 spring Y1."); - // handle season transition - if (day > this.DaysInSeason || day < 1) - { - // get season index - int curSeasonIndex = this.GetSeasonIndex(); - - // get season offset - int seasonOffset = day / this.DaysInSeason; - if (day < 1) - seasonOffset -= 1; - - // get new date - day = this.GetWrappedIndex(day, this.DaysInSeason); - season = this.Seasons[this.GetWrappedIndex(curSeasonIndex + seasonOffset, this.Seasons.Length)]; - year += seasonOffset / this.Seasons.Length; - } + // get day + int day = hashCode % 28; + if (day == 0) + day = 28; - // validate - if (year < 1) - throw new ArithmeticException($"Adding {offset} days to {this} would result in invalid date {day:00} {season} {year}."); + // get season index + int seasonIndex = hashCode / 28; + if (seasonIndex > 0 && hashCode % 28 == 0) + seasonIndex -= 1; + seasonIndex %= 4; - // return new date - return new SDate(day, season, year); + // get year + int year = hashCode / (this.Seasons.Length * this.DaysInSeason) + 1; + + // create date + return new SDate(day, this.Seasons[seasonIndex], year); } /// Get a string representation of the date. This is mainly intended for debugging or console messages. @@ -142,7 +135,7 @@ namespace StardewModdingAPI.Utilities /// Get a hash code which uniquely identifies a date. public override int GetHashCode() { - // return the number of days since 01 spring Y1 + // return the number of days since 01 spring Y1 (inclusively) int yearIndex = this.Year - 1; return yearIndex * this.DaysInSeason * this.SeasonsInYear @@ -239,16 +232,5 @@ namespace StardewModdingAPI.Utilities throw new InvalidOperationException($"The current season '{this.Season}' wasn't recognised."); return index; } - - /// Get the real index in an array which should be treated as a two-way loop. - /// The index in the looped array. - /// The number of elements in the array. - private int GetWrappedIndex(int index, int length) - { - int wrapped = index % length; - if (wrapped < 0) - wrapped += length; - return wrapped; - } } } -- cgit From c994747e513f98eb5760a3a4dc9e063b16b1abc9 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 6 Sep 2017 23:22:11 -0400 Subject: add UTC timestamp to logs --- release-notes.md | 1 + src/StardewModdingAPI/Program.cs | 1 + 2 files changed, 2 insertions(+) (limited to 'src') diff --git a/release-notes.md b/release-notes.md index e5721fd9..7b8c83e3 100644 --- a/release-notes.md +++ b/release-notes.md @@ -32,6 +32,7 @@ For players: For modders: * Fixed edge cases in `SDate.AddDays(…)` calculations. +* Added UTC timestamp to logs. ## 1.15.3 For players: diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index 13d52f12..ad873598 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -128,6 +128,7 @@ namespace StardewModdingAPI // init logging this.Monitor.Log($"SMAPI {Constants.ApiVersion} with Stardew Valley {Constants.GameVersion} on {this.GetFriendlyPlatformName()}", LogLevel.Info); this.Monitor.Log($"Mods go here: {Constants.ModPath}"); + this.Monitor.Log($"Log started at {DateTime.UtcNow:s} UTC", LogLevel.Trace); #if SMAPI_1_x this.Monitor.Log("Preparing SMAPI..."); #endif -- cgit From 1825755e75f5ea9a339d96d591cbcf0f1b950f4d Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 8 Sep 2017 11:50:49 -0400 Subject: update for release --- release-notes.md | 9 ++++++--- src/GlobalAssemblyInfo.cs | 4 ++-- src/StardewModdingAPI/Constants.cs | 2 +- src/TrainerMod/manifest.json | 4 ++-- 4 files changed, 11 insertions(+), 8 deletions(-) (limited to 'src') diff --git a/release-notes.md b/release-notes.md index 7b8c83e3..16ebae2c 100644 --- a/release-notes.md +++ b/release-notes.md @@ -28,11 +28,14 @@ For power users: ## 1.15.4 For players: -* Fixed issues with maps loaded through Entoarox Framework. +* Fixed errors when loading some custom maps via Entoarox Framework or XNB Loader. +* Fixed errors with in-game date calculation in some mods. For modders: -* Fixed edge cases in `SDate.AddDays(…)` calculations. -* Added UTC timestamp to logs. +* Added UTC timestamp to log file. + +For SMAPI developers: +* Internal changes to support the upcoming SMAPI 2.0 release. ## 1.15.3 For players: diff --git a/src/GlobalAssemblyInfo.cs b/src/GlobalAssemblyInfo.cs index 4b15d3aa..882e3bda 100644 --- a/src/GlobalAssemblyInfo.cs +++ b/src/GlobalAssemblyInfo.cs @@ -2,5 +2,5 @@ using System.Reflection; using System.Runtime.InteropServices; [assembly: ComVisible(false)] -[assembly: AssemblyVersion("1.15.3.0")] -[assembly: AssemblyFileVersion("1.15.3.0")] +[assembly: AssemblyVersion("1.15.4.0")] +[assembly: AssemblyFileVersion("1.15.4.0")] diff --git a/src/StardewModdingAPI/Constants.cs b/src/StardewModdingAPI/Constants.cs index 8ed4f416..fea9377a 100644 --- a/src/StardewModdingAPI/Constants.cs +++ b/src/StardewModdingAPI/Constants.cs @@ -36,7 +36,7 @@ namespace StardewModdingAPI /// SMAPI's current semantic version. public static ISemanticVersion ApiVersion { get; } = #if SMAPI_1_x - new SemanticVersion(1, 15, 3); + new SemanticVersion(1, 15, 4); #else new SemanticVersion(2, 0, 0, $"alpha-{DateTime.UtcNow:yyyyMMddHHmm}"); #endif diff --git a/src/TrainerMod/manifest.json b/src/TrainerMod/manifest.json index 5c634f53..20b40f8a 100644 --- a/src/TrainerMod/manifest.json +++ b/src/TrainerMod/manifest.json @@ -1,10 +1,10 @@ -{ +{ "Name": "Trainer Mod", "Author": "SMAPI", "Version": { "MajorVersion": 1, "MinorVersion": 15, - "PatchVersion": 1, + "PatchVersion": 4, "Build": null }, "Description": "Adds SMAPI console commands that let you manipulate the game.", -- cgit From 23951220ae84f3132832c942b61a8e81aee1fbfe Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 8 Sep 2017 13:18:43 -0400 Subject: fix errors loading some custom map tilesheets on Linux/Mac --- release-notes.md | 4 ++-- src/StardewModdingAPI/Framework/ModHelpers/ContentHelper.cs | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/release-notes.md b/release-notes.md index 16ebae2c..5e01a264 100644 --- a/release-notes.md +++ b/release-notes.md @@ -28,8 +28,8 @@ For power users: ## 1.15.4 For players: -* Fixed errors when loading some custom maps via Entoarox Framework or XNB Loader. -* Fixed errors with in-game date calculation in some mods. +* Fixed errors when loading some custom maps on Linux/Mac or using XNB Loader. +* Fixed errors in rare cases when a mod calculates an in-game date. For modders: * Added UTC timestamp to log file. diff --git a/src/StardewModdingAPI/Framework/ModHelpers/ContentHelper.cs b/src/StardewModdingAPI/Framework/ModHelpers/ContentHelper.cs index 9f236823..4440ae40 100644 --- a/src/StardewModdingAPI/Framework/ModHelpers/ContentHelper.cs +++ b/src/StardewModdingAPI/Framework/ModHelpers/ContentHelper.cs @@ -231,6 +231,7 @@ namespace StardewModdingAPI.Framework.ModHelpers // check map info if (!map.TileSheets.Any()) return; + mapKey = this.ContentManager.NormaliseAssetName(mapKey); // Mono's Path.GetDirectoryName doesn't handle Windows dir separators string relativeMapFolder = Path.GetDirectoryName(mapKey) ?? ""; // folder path containing the map, relative to the mod folder // fix tilesheets -- cgit